From 780525168570480191c2ab98cf1813254c3e4eed Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 8 Jan 2023 19:17:11 +0000 Subject: [PATCH 01/35] Add ISY994 variables as number entities --- .coveragerc | 1 + homeassistant/components/isy994/__init__.py | 9 +- homeassistant/components/isy994/const.py | 9 + homeassistant/components/isy994/helpers.py | 19 --- homeassistant/components/isy994/number.py | 154 ++++++++++++++++++ homeassistant/components/isy994/sensor.py | 37 +---- homeassistant/components/isy994/services.py | 14 ++ homeassistant/components/isy994/services.yaml | 4 +- 8 files changed, 189 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/isy994/number.py diff --git a/.coveragerc b/.coveragerc index cad9ca4fe742f3..858c1761566dea 100644 --- a/.coveragerc +++ b/.coveragerc @@ -604,6 +604,7 @@ omit = homeassistant/components/isy994/helpers.py homeassistant/components/isy994/light.py homeassistant/components/isy994/lock.py + homeassistant/components/isy994/number.py homeassistant/components/isy994/sensor.py homeassistant/components/isy994/services.py homeassistant/components/isy994/switch.py diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index f0a40a03cfacce..dec7a3874432dd 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -43,7 +43,7 @@ PROGRAM_PLATFORMS, SENSOR_AUX, ) -from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables +from .helpers import _categorize_nodes, _categorize_programs from .services import async_setup_services, async_unload_services from .util import unique_ids_for_config_entry_id @@ -201,7 +201,12 @@ async def async_setup_entry( _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) _categorize_programs(hass_isy_data, isy.programs) - _categorize_variables(hass_isy_data, isy.variables, variable_identifier) + + # Gather ISY Variables to be added. Identifier used to enable by default. + for vtype, vname, vid in isy.variables.children: + hass_isy_data[ISY994_VARIABLES].append( + (isy.variables[vtype][vid], variable_identifier in vname) + ) # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs _LOGGER.info(repr(isy.clock)) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index de064bff312ef3..1e9733395f4302 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -82,6 +82,7 @@ Platform.FAN, Platform.LIGHT, Platform.LOCK, + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] @@ -297,6 +298,14 @@ FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE], FILTER_ZWAVE_CAT: ["140"], }, + Platform.NUMBER: { + # No devices automatically sorted as numbers at this time. + FILTER_UOM: [], + FILTER_STATES: [], + FILTER_NODE_DEF_ID: [], + FILTER_INSTEON_TYPE: [], + FILTER_ZWAVE_CAT: [], + }, } UOM_FRIENDLY_NAME = { diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 54d2890c84c499..bf9dd190f4c1ee 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -14,7 +14,6 @@ ) from pyisy.nodes import Group, Node, Nodes from pyisy.programs import Programs -from pyisy.variables import Variables from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -31,7 +30,6 @@ FILTER_ZWAVE_CAT, ISY994_NODES, ISY994_PROGRAMS, - ISY994_VARIABLES, ISY_GROUP_PLATFORM, KEY_ACTIONS, KEY_STATUS, @@ -363,23 +361,6 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: hass_isy_data[ISY994_PROGRAMS][platform].append(entity) -def _categorize_variables( - hass_isy_data: dict, variables: Variables, identifier: str -) -> None: - """Gather the ISY Variables to be added as sensors.""" - try: - var_to_add = [ - (vtype, vname, vid) - for (vtype, vname, vid) in variables.children - if identifier in vname - ] - except KeyError as err: - _LOGGER.error("Error adding ISY Variables: %s", err) - return - for vtype, vname, vid in var_to_add: - hass_isy_data[ISY994_VARIABLES].append((vname, variables[vtype][vid])) - - async def migrate_old_unique_ids( hass: HomeAssistant, platform: str, entities: Sequence[ISYEntity] ) -> None: diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py new file mode 100644 index 00000000000000..59ea2941639d60 --- /dev/null +++ b/homeassistant/components/isy994/number.py @@ -0,0 +1,154 @@ +"""GoodWe PV inverter numeric settings entities.""" +from __future__ import annotations + +from typing import Any + +from pyisy import ISY +from pyisy.helpers import EventListener, NodeProperty +from pyisy.variables import Variable + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import _async_isy_to_configuration_url +from .const import DOMAIN as ISY994_DOMAIN, ISY994_ISY, ISY994_VARIABLES, MANUFACTURER +from .helpers import convert_isy_value_to_hass + +ISY_CONF_UUID = "uuid" # TODO: Remove and import when PR#85429 is merged +ISY_CONF_FIRMWARE = "firmware" +ISY_CONF_MODEL = "model" +ISY_CONF_NAME = "name" + +ISY_MAX_SIZE = (2**32) / 2 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX number entities from config entry.""" + hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] + isy: ISY = hass_isy_data[ISY994_ISY] + uuid = isy.configuration[ISY_CONF_UUID] + entities: list[ISYVariableNumberEntity] = [] + + for node, enable_by_default in hass_isy_data[ISY994_VARIABLES]: + description = NumberEntityDescription( + key=node.address, + name=node.name, + icon="mdi:counter", + native_unit_of_measurement=None, + native_step=10 ** (-1 * node.prec), + native_min_value=-ISY_MAX_SIZE / (10**node.prec), + native_max_value=ISY_MAX_SIZE / (10**node.prec), + ) + description_init = NumberEntityDescription( + key=f"{node.address}_init", + name=f"{node.name} Initial Value", + icon="mdi:counter", + native_unit_of_measurement=None, + native_step=10 ** (-1 * node.prec), + native_min_value=-ISY_MAX_SIZE / (10**node.prec), + native_max_value=ISY_MAX_SIZE / (10**node.prec), + entity_category=EntityCategory.CONFIG, + ) + + entities.append( + ISYVariableNumberEntity( + node, + unique_id=f"{uuid}_{node.address}", + description=description, + enable_by_default=enable_by_default, + ) + ) + entities.append( + ISYVariableNumberEntity( + node=node, + unique_id=f"{uuid}_{node.address}_init", + description=description_init, + enable_by_default=False, + init_entity=True, + ) + ) + + async_add_entities(entities) + + +class ISYVariableNumberEntity(NumberEntity): + """Representation of an ISY variable as a number entity device.""" + + _attr_has_entity_name = True + _attr_should_poll = False + _init_entity: bool + _node: Variable + entity_description: NumberEntityDescription + + def __init__( + self, + node: Variable, + unique_id: str, + description: NumberEntityDescription, + enable_by_default: bool, + init_entity: bool = False, + ) -> None: + """Initialize the ISY binary sensor program.""" + self._node = node + self._name = description.name + self.entity_description = description + self._change_handler: EventListener | None = None + + # Two entities are created for each variable, one for current value and one for initial. + # Initial value entities are disabled by default + self._init_entity = init_entity + self._attr_entity_registry_enabled_default = enable_by_default + + self._attr_unique_id = unique_id + + url = _async_isy_to_configuration_url(node.isy) + self._attr_device_info = DeviceInfo( + identifiers={ + ( + ISY994_DOMAIN, + f"{node.isy.configuration[ISY_CONF_UUID]}_variables", + ) + }, + manufacturer=MANUFACTURER, + name=f"{node.isy.configuration[ISY_CONF_NAME]} Variables", + model=node.isy.configuration[ISY_CONF_MODEL], + sw_version=node.isy.configuration[ISY_CONF_FIRMWARE], + configuration_url=url, + via_device=(ISY994_DOMAIN, node.isy.configuration[ISY_CONF_UUID]), + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + self._change_handler = self._node.status_events.subscribe(self.async_on_update) + + @callback + def async_on_update(self, event: NodeProperty) -> None: + """Handle the update event from the ISY Node.""" + self.async_write_ha_state() + + @property + def native_value(self) -> float | int | None: + """Return the state of the variable.""" + return convert_isy_value_to_hass( + self._node.init if self._init_entity else self._node.status, + "", + self._node.prec, + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Get the state attributes for the device.""" + return { + "last_edited": self._node.last_edited, + } + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self._node.set_value(value, init=self._init_entity) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 4a80229eef7e2b..6efe34c1d87c49 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -35,7 +35,6 @@ _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, - ISY994_VARIABLES, SENSOR_AUX, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, @@ -43,7 +42,7 @@ UOM_ON_OFF, UOM_TO_STATES, ) -from .entity import ISYEntity, ISYNodeEntity +from .entity import ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids # Disable general purpose and redundant sensors by default @@ -110,7 +109,7 @@ async def async_setup_entry( ) -> None: """Set up the ISY sensor platform.""" hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] + entities: list[ISYSensorEntity] = [] for node in hass_isy_data[ISY994_NODES][Platform.SENSOR]: _LOGGER.debug("Loading %s", node.name) @@ -131,9 +130,6 @@ async def async_setup_entry( # Any node in SENSOR_AUX can potentially have communication errors entities.append(ISYAuxSensorEntity(node, PROP_COMMS_ERROR, False)) - for vname, vobj in hass_isy_data[ISY994_VARIABLES]: - entities.append(ISYSensorVariableEntity(vname, vobj)) - await migrate_old_unique_ids(hass, Platform.SENSOR, entities) async_add_entities(entities) @@ -263,32 +259,3 @@ def name(self) -> str: base_name = self._name or str(self._node.name) name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) return f"{base_name} {name.replace('_', ' ').title()}" - - -class ISYSensorVariableEntity(ISYEntity, SensorEntity): - """Representation of an ISY variable as a sensor device.""" - - def __init__(self, vname: str, vobj: object) -> None: - """Initialize the ISY binary sensor program.""" - super().__init__(vobj) - self._name = vname - - @property - def native_value(self) -> float | int | None: - """Return the state of the variable.""" - return convert_isy_value_to_hass(self._node.status, "", self._node.prec) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Get the state attributes for the device.""" - return { - "init_value": convert_isy_value_to_hass( - self._node.init, "", self._node.prec - ), - "last_edited": self._node.last_edited, - } - - @property - def icon(self) -> str: - """Return the icon.""" - return "mdi:counter" diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 6825cab7cd20d7..0ab328484e2797 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -26,6 +26,8 @@ from .const import _LOGGER, DOMAIN, ISY994_ISY from .util import unique_ids_for_config_entry_id +ISY_CONF_UUID = "uuid" # TODO: Remove and import when PR#85429 is merged + # Common Services for All Platforms: SERVICE_SYSTEM_QUERY = "system_query" SERVICE_SET_VARIABLE = "set_variable" @@ -288,6 +290,18 @@ async def async_set_variable_service_handler(service: ServiceCall) -> None: variable = isy.variables.vobjs[vtype].get(address) if variable is not None: await variable.set_value(value, init) + entity_registry = er.async_get(hass) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="number.set_value", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{isy.configuration[ISY_CONF_UUID]}_{address}{'_init' if init else ''}", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.error("Could not set variable value; not found or enabled on the ISY") diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 90715b162d702c..0e82df2b3560b5 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -184,8 +184,8 @@ system_query: selector: text: set_variable: - name: Set variable - description: Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. + name: Set variable (Deprecated) + description: "Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. Deprecated: Use number entities instead." fields: address: name: Address From 8aa9581a7814b204370cf7ad31ec87237e9f9fbd Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 9 Jan 2023 01:39:33 +0000 Subject: [PATCH 02/35] Cleanup based on other reviews --- homeassistant/components/isy994/number.py | 33 ++++++++++++----------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index 59ea2941639d60..e82bfc3f9267f6 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -22,7 +22,7 @@ ISY_CONF_MODEL = "model" ISY_CONF_NAME = "name" -ISY_MAX_SIZE = (2**32) / 2 +ISY_MAX_SIZE = (2**32) / 2 # 32-bit signed integer async def async_setup_entry( @@ -37,23 +37,27 @@ async def async_setup_entry( entities: list[ISYVariableNumberEntity] = [] for node, enable_by_default in hass_isy_data[ISY994_VARIABLES]: + step = 10 ** (-1 * node.prec) + min_max = ISY_MAX_SIZE / (10**node.prec) description = NumberEntityDescription( key=node.address, name=node.name, icon="mdi:counter", + entity_registry_enabled_default=enable_by_default, native_unit_of_measurement=None, - native_step=10 ** (-1 * node.prec), - native_min_value=-ISY_MAX_SIZE / (10**node.prec), - native_max_value=ISY_MAX_SIZE / (10**node.prec), + native_step=step, + native_min_value=-min_max, + native_max_value=min_max, ) description_init = NumberEntityDescription( key=f"{node.address}_init", name=f"{node.name} Initial Value", icon="mdi:counter", + entity_registry_enabled_default=False, native_unit_of_measurement=None, - native_step=10 ** (-1 * node.prec), - native_min_value=-ISY_MAX_SIZE / (10**node.prec), - native_max_value=ISY_MAX_SIZE / (10**node.prec), + native_step=step, + native_min_value=-min_max, + native_max_value=min_max, entity_category=EntityCategory.CONFIG, ) @@ -62,7 +66,6 @@ async def async_setup_entry( node, unique_id=f"{uuid}_{node.address}", description=description, - enable_by_default=enable_by_default, ) ) entities.append( @@ -70,7 +73,6 @@ async def async_setup_entry( node=node, unique_id=f"{uuid}_{node.address}_init", description=description_init, - enable_by_default=False, init_entity=True, ) ) @@ -92,7 +94,6 @@ def __init__( node: Variable, unique_id: str, description: NumberEntityDescription, - enable_by_default: bool, init_entity: bool = False, ) -> None: """Initialize the ISY binary sensor program.""" @@ -104,24 +105,24 @@ def __init__( # Two entities are created for each variable, one for current value and one for initial. # Initial value entities are disabled by default self._init_entity = init_entity - self._attr_entity_registry_enabled_default = enable_by_default self._attr_unique_id = unique_id url = _async_isy_to_configuration_url(node.isy) + config = node.isy.configuration self._attr_device_info = DeviceInfo( identifiers={ ( ISY994_DOMAIN, - f"{node.isy.configuration[ISY_CONF_UUID]}_variables", + f"{config[ISY_CONF_UUID]}_variables", ) }, manufacturer=MANUFACTURER, - name=f"{node.isy.configuration[ISY_CONF_NAME]} Variables", - model=node.isy.configuration[ISY_CONF_MODEL], - sw_version=node.isy.configuration[ISY_CONF_FIRMWARE], + name=f"{config[ISY_CONF_NAME]} Variables", + model=config[ISY_CONF_MODEL], + sw_version=config[ISY_CONF_FIRMWARE], configuration_url=url, - via_device=(ISY994_DOMAIN, node.isy.configuration[ISY_CONF_UUID]), + via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]), ) async def async_added_to_hass(self) -> None: From 30160e323c173cfbfded8bdbe9976d721f1359b1 Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 9 Jan 2023 01:41:02 +0000 Subject: [PATCH 03/35] Remove unnecessary descriptors --- homeassistant/components/isy994/number.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py index e82bfc3f9267f6..fd4320d1a7b50e 100644 --- a/homeassistant/components/isy994/number.py +++ b/homeassistant/components/isy994/number.py @@ -42,9 +42,7 @@ async def async_setup_entry( description = NumberEntityDescription( key=node.address, name=node.name, - icon="mdi:counter", entity_registry_enabled_default=enable_by_default, - native_unit_of_measurement=None, native_step=step, native_min_value=-min_max, native_max_value=min_max, @@ -52,9 +50,7 @@ async def async_setup_entry( description_init = NumberEntityDescription( key=f"{node.address}_init", name=f"{node.name} Initial Value", - icon="mdi:counter", entity_registry_enabled_default=False, - native_unit_of_measurement=None, native_step=step, native_min_value=-min_max, native_max_value=min_max, From e8d97d9b6260486861bb733c66aeda05d2ce9b22 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 8 Jan 2023 12:57:46 -0500 Subject: [PATCH 04/35] Limit Whirlpool timestamp changes to +/- 60 seconds (#85368) * Limit timestamp changes to +/- 60 seconds * Add timestamp callback tests --- homeassistant/components/whirlpool/sensor.py | 9 +- tests/components/whirlpool/conftest.py | 4 + tests/components/whirlpool/test_sensor.py | 102 ++++++++++++++++++- 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py index 41337aea9bd78e..8a8df82eb55aee 100644 --- a/homeassistant/components/whirlpool/sensor.py +++ b/homeassistant/components/whirlpool/sensor.py @@ -280,8 +280,13 @@ def update_from_latest_data(self) -> None: if machine_state is MachineState.RunningMainCycle: self._running = True - self._attr_native_value = now + timedelta( + new_timestamp = now + timedelta( seconds=int(self._wd.get_attribute("Cavity_TimeStatusEstTimeRemaining")) ) - self._async_write_ha_state() + if isinstance(self._attr_native_value, datetime) and abs( + new_timestamp - self._attr_native_value + ) > timedelta(seconds=60): + + self._attr_native_value = new_timestamp + self._async_write_ha_state() diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index e411cfb8c2d4d5..2fad5913749632 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -63,6 +63,7 @@ def get_aircon_mock(said): mock_aircon = mock.Mock(said=said) mock_aircon.connect = AsyncMock() mock_aircon.disconnect = AsyncMock() + mock_aircon.register_attr_callback = AsyncMock() mock_aircon.get_online.return_value = True mock_aircon.get_power_on.return_value = True mock_aircon.get_mode.return_value = whirlpool.aircon.Mode.Cool @@ -114,6 +115,8 @@ def side_effect_function(*args, **kwargs): return "0" if args[0] == "WashCavity_OpStatusBulkDispense1Level": return "3" + if args[0] == "Cavity_TimeStatusEstTimeRemaining": + return "4000" def get_sensor_mock(said): @@ -121,6 +124,7 @@ def get_sensor_mock(said): mock_sensor = mock.Mock(said=said) mock_sensor.connect = AsyncMock() mock_sensor.disconnect = AsyncMock() + mock_sensor.register_attr_callback = AsyncMock() mock_sensor.get_online.return_value = True mock_sensor.get_machine_state.return_value = ( whirlpool.washerdryer.MachineState.Standby diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py index 2da53521a055a3..658613b48c1b1f 100644 --- a/tests/components/whirlpool/test_sensor.py +++ b/tests/components/whirlpool/test_sensor.py @@ -6,6 +6,7 @@ from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.helpers import entity_registry +from homeassistant.util.dt import as_timestamp, utc_from_timestamp from . import init_integration @@ -45,6 +46,25 @@ async def test_dryer_sensor_values( mock_sensor2_api: MagicMock, ): """Test the sensor value callbacks.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + await init_integration(hass) entity_id = "sensor.dryer_state" @@ -60,7 +80,7 @@ async def test_dryer_sensor_values( assert state is not None state_id = f"{entity_id.split('_')[0]}_end_time" state = hass.states.get(state_id) - assert state is not None + assert state.state == thetimestamp.isoformat() mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle mock_instance.get_cycle_status_filling.return_value = False @@ -90,6 +110,25 @@ async def test_washer_sensor_values( mock_sensor1_api: MagicMock, ): """Test the sensor value callbacks.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + await init_integration(hass) entity_id = "sensor.washer_state" @@ -105,7 +144,7 @@ async def test_washer_sensor_values( assert state is not None state_id = f"{entity_id.split('_')[0]}_end_time" state = hass.states.get(state_id) - assert state is not None + assert state.state == thetimestamp.isoformat() state_id = f"{entity_id.split('_')[0]}_detergent_level" state = hass.states.get(state_id) @@ -243,3 +282,62 @@ async def test_restore_state( assert state.state == thetimestamp.isoformat() state = hass.states.get("sensor.dryer_end_time") assert state.state == thetimestamp.isoformat() + + +async def test_callback( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, + mock_sensor1_api: MagicMock, +): + """Test callback timestamp callback function.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + + # create and add entry + await init_integration(hass) + # restore from cache + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + callback = mock_sensor1_api.register_attr_callback.call_args_list[2][0][0] + callback() + # await hass.async_block_till_done() + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + mock_sensor1_api.get_machine_state.return_value = MachineState.RunningMainCycle + mock_sensor1_api.get_attribute.side_effect = None + mock_sensor1_api.get_attribute.return_value = "60" + callback() + + # Test new timestamp when machine starts a cycle. + state = hass.states.get("sensor.washer_end_time") + time = state.state + assert state.state != thetimestamp.isoformat() + + # Test no timestamp change for < 60 seconds time change. + mock_sensor1_api.get_attribute.return_value = "65" + callback() + state = hass.states.get("sensor.washer_end_time") + assert state.state == time + + # Test timestamp change for > 60 seconds. + mock_sensor1_api.get_attribute.return_value = "120" + callback() + state = hass.states.get("sensor.washer_end_time") + newtime = utc_from_timestamp(as_timestamp(time) + 60) + assert state.state == newtime.isoformat() From 149a5a9d231f8834b5b88ce945bd1ea3fda1b939 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Jan 2023 09:42:29 -1000 Subject: [PATCH 05/35] Small speed up to frequently called datetime functions (#85399) --- homeassistant/util/dt.py | 21 ++++++---- tests/common.py | 6 +-- tests/components/recorder/test_history.py | 50 +++++++++++------------ tests/conftest.py | 9 ++++ 4 files changed, 50 insertions(+), 36 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 44e4403d689dd5..3e9ae088296ade 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,6 +4,7 @@ import bisect from contextlib import suppress import datetime as dt +from functools import partial import platform import re import time @@ -98,9 +99,10 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: return None -def utcnow() -> dt.datetime: - """Get now in UTC time.""" - return dt.datetime.now(UTC) +# We use a partial here since it is implemented in native code +# and avoids the global lookup of UTC +utcnow: partial[dt.datetime] = partial(dt.datetime.now, UTC) +utcnow.__doc__ = "Get now in UTC time." def now(time_zone: dt.tzinfo | None = None) -> dt.datetime: @@ -466,8 +468,8 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() -def __monotonic_time_coarse() -> float: - """Return a monotonic time in seconds. +def __gen_monotonic_time_coarse() -> partial[float]: + """Return a function that provides monotonic time in seconds. This is the coarse version of time_monotonic, which is faster but less accurate. @@ -477,13 +479,16 @@ def __monotonic_time_coarse() -> float: https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ """ - return time.clock_gettime(CLOCK_MONOTONIC_COARSE) + # We use a partial here since its implementation is in native code + # which allows us to avoid the overhead of the global lookup + # of CLOCK_MONOTONIC_COARSE. + return partial(time.clock_gettime, CLOCK_MONOTONIC_COARSE) monotonic_time_coarse = time.monotonic with suppress(Exception): if ( platform.system() == "Linux" - and abs(time.monotonic() - __monotonic_time_coarse()) < 1 + and abs(time.monotonic() - __gen_monotonic_time_coarse()()) < 1 ): - monotonic_time_coarse = __monotonic_time_coarse + monotonic_time_coarse = __gen_monotonic_time_coarse() diff --git a/tests/common.py b/tests/common.py index eb7c7ba63ab98f..eaa31851f0c2c5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,7 +5,7 @@ from collections import OrderedDict from collections.abc import Awaitable, Callable, Collection from contextlib import contextmanager -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import functools as ft from io import StringIO import json @@ -396,7 +396,7 @@ def async_fire_time_changed_exact( approach, as this is only for testing. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: utc_datetime = date_util.as_utc(datetime_) @@ -418,7 +418,7 @@ def async_fire_time_changed( for an exact microsecond, use async_fire_time_changed_exact. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: utc_datetime = date_util.as_utc(datetime_) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 913ae3d8bf603c..0465c10a8d24c1 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -7,7 +7,6 @@ import json from unittest.mock import patch, sentinel -from freezegun import freeze_time import pytest from sqlalchemy import text @@ -973,6 +972,7 @@ def test_state_changes_during_period_multiple_entities_single_test(hass_recorder hist[entity_id][0].state == value +@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") async def test_get_full_significant_states_past_year_2038( async_setup_recorder_instance: SetupRecorderInstanceT, hass: ha.HomeAssistant, @@ -980,29 +980,29 @@ async def test_get_full_significant_states_past_year_2038( """Test we can store times past year 2038.""" await async_setup_recorder_instance(hass, {}) past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() - with freeze_time(past_2038_time): - hass.states.async_set("sensor.one", "on", {"attr": "original"}) - state0 = hass.states.get("sensor.one") - await hass.async_block_till_done() - hass.states.async_set("sensor.one", "on", {"attr": "new"}) - state1 = hass.states.get("sensor.one") - await async_wait_recording_done(hass) - - def _get_entries(): - with session_scope(hass=hass) as session: - return history.get_full_significant_states_with_session( - hass, - session, - past_2038_time - timedelta(days=365), - past_2038_time + timedelta(days=365), - entity_ids=["sensor.one"], - significant_changes_only=False, - ) + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + await async_wait_recording_done(hass) - states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) - sensor_one_states: list[State] = states["sensor.one"] - assert sensor_one_states[0] == state0 - assert sensor_one_states[1] == state1 - assert sensor_one_states[0].last_changed == past_2038_time - assert sensor_one_states[0].last_updated == past_2038_time + def _get_entries(): + with session_scope(hass=hass) as session: + return history.get_full_significant_states_with_session( + hass, + session, + past_2038_time - timedelta(days=365), + past_2038_time + timedelta(days=365), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert sensor_one_states[0] == state0 + assert sensor_one_states[1] == state1 + assert sensor_one_states[0].last_changed == past_2038_time + assert sensor_one_states[0].last_updated == past_2038_time diff --git a/tests/conftest.py b/tests/conftest.py index 307f6626ba8085..75655cf2d8682b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import asyncio from collections.abc import AsyncGenerator, Callable, Generator from contextlib import asynccontextmanager +import datetime import functools import gc import itertools @@ -78,6 +79,14 @@ asyncio.set_event_loop_policy = lambda policy: None +def _utcnow(): + """Make utcnow patchable by freezegun.""" + return datetime.datetime.now(datetime.timezone.utc) + + +dt_util.utcnow = _utcnow + + def pytest_addoption(parser): """Register custom pytest options.""" parser.addoption("--dburl", action="store", default="sqlite://") From 6d017ba85212ae6d9a568957143ffef59b014ebd Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 8 Jan 2023 13:50:18 -0600 Subject: [PATCH 06/35] Use subscription callbacks to discover Sonos speakers (#85411) fixes undefined --- homeassistant/components/sonos/__init__.py | 156 ++++++++++++++---- homeassistant/components/sonos/speaker.py | 29 +++- tests/components/sonos/conftest.py | 26 ++- .../sonos/fixtures/zgs_discovery.xml | 7 + tests/components/sonos/test_config_flow.py | 8 +- tests/components/sonos/test_sensor.py | 5 +- tests/components/sonos/test_switch.py | 5 +- 7 files changed, 189 insertions(+), 47 deletions(-) create mode 100644 tests/components/sonos/fixtures/zgs_discovery.xml diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 2f003e4bde99db..45b78cd0dd6b4d 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -11,9 +11,10 @@ from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import urlparse -from soco import events_asyncio +from soco import events_asyncio, zonegroupstate import soco.config as soco_config from soco.core import SoCo +from soco.events_base import Event as SonosEvent, SubscriptionBase from soco.exceptions import SoCoException import voluptuous as vol @@ -24,8 +25,8 @@ from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send -from homeassistant.helpers.event import async_track_time_interval, call_later +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType from .alarms import SonosAlarms @@ -40,6 +41,7 @@ SONOS_REBOOTED, SONOS_SPEAKER_ACTIVITY, SONOS_VANISHED, + SUBSCRIPTION_TIMEOUT, UPNP_ST, ) from .exception import SonosUpdateError @@ -51,7 +53,7 @@ CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" DISCOVERY_IGNORED_MODELS = ["Sonos Boost"] - +ZGS_SUBSCRIPTION_TIMEOUT = 2 CONFIG_SCHEMA = vol.Schema( { @@ -122,6 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" soco_config.EVENTS_MODULE = events_asyncio soco_config.REQUEST_TIMEOUT = 9.5 + zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() @@ -172,6 +175,7 @@ def __init__( self.data = data self.hosts = set(hosts) self.discovery_lock = asyncio.Lock() + self.creation_lock = asyncio.Lock() self._known_invisible: set[SoCo] = set() self._manual_config_required = bool(hosts) @@ -184,21 +188,70 @@ def is_device_invisible(self, ip_address: str) -> bool: """Check if device at provided IP is known to be invisible.""" return any(x for x in self._known_invisible if x.ip_address == ip_address) - def _create_visible_speakers(self, ip_address: str) -> None: - """Create all visible SonosSpeaker instances with the provided seed IP.""" - try: - soco = SoCo(ip_address) + async def async_subscribe_to_zone_updates(self, ip_address: str) -> None: + """Test subscriptions and create SonosSpeakers based on results.""" + soco = SoCo(ip_address) + # Cache now to avoid household ID lookup during first ZoneGroupState processing + await self.hass.async_add_executor_job( + getattr, + soco, + "household_id", + ) + sub = await soco.zoneGroupTopology.subscribe() + + @callback + def _async_add_visible_zones(subscription_succeeded: bool = False) -> None: + """Determine visible zones and create SonosSpeaker instances.""" + zones_to_add = set() + subscription = None + if subscription_succeeded: + subscription = sub + visible_zones = soco.visible_zones self._known_invisible = soco.all_zones - visible_zones - except (OSError, SoCoException) as ex: + for zone in visible_zones: + if zone.uid not in self.data.discovered: + zones_to_add.add(zone) + + if not zones_to_add: + return + + self.hass.async_create_task( + self.async_add_speakers(zones_to_add, subscription, soco.uid) + ) + + async def async_subscription_failed(now: datetime.datetime) -> None: + """Fallback logic if the subscription callback never arrives.""" + await sub.unsubscribe() _LOGGER.warning( - "Failed to request visible zones from %s: %s", ip_address, ex + "Subscription to %s failed, attempting to poll directly", ip_address ) - return + try: + await self.hass.async_add_executor_job(soco.zone_group_state.poll, soco) + except (OSError, SoCoException) as ex: + _LOGGER.warning( + "Fallback pollling to %s failed, setup cannot continue: %s", + ip_address, + ex, + ) + return + _LOGGER.debug("Fallback ZoneGroupState poll to %s succeeded", ip_address) + _async_add_visible_zones() + + cancel_failure_callback = async_call_later( + self.hass, ZGS_SUBSCRIPTION_TIMEOUT, async_subscription_failed + ) + + @callback + def _async_subscription_succeeded(event: SonosEvent) -> None: + """Create SonosSpeakers when subscription callbacks successfully arrive.""" + _LOGGER.debug("Subscription to %s succeeded", ip_address) + cancel_failure_callback() + _async_add_visible_zones(subscription_succeeded=True) - for zone in visible_zones: - if zone.uid not in self.data.discovered: - self._add_speaker(zone) + sub.callback = _async_subscription_succeeded + # Hold lock to prevent concurrent subscription attempts + await asyncio.sleep(ZGS_SUBSCRIPTION_TIMEOUT * 2) async def _async_stop_event_listener(self, event: Event | None = None) -> None: for speaker in self.data.discovered.values(): @@ -227,14 +280,35 @@ def _stop_manual_heartbeat(self, event: Event | None = None) -> None: self.data.hosts_heartbeat() self.data.hosts_heartbeat = None - def _add_speaker(self, soco: SoCo) -> None: + async def async_add_speakers( + self, + socos: set[SoCo], + zgs_subscription: SubscriptionBase | None, + zgs_subscription_uid: str | None, + ) -> None: + """Create and set up new SonosSpeaker instances.""" + + def _add_speakers(): + """Add all speakers in a single executor job.""" + for soco in socos: + sub = None + if soco.uid == zgs_subscription_uid and zgs_subscription: + sub = zgs_subscription + self._add_speaker(soco, sub) + + async with self.creation_lock: + await self.hass.async_add_executor_job(_add_speakers) + + def _add_speaker( + self, soco: SoCo, zone_group_state_sub: SubscriptionBase | None + ) -> None: """Create and set up a new SonosSpeaker instance.""" try: speaker_info = soco.get_speaker_info(True, timeout=7) if soco.uid not in self.data.boot_counts: self.data.boot_counts[soco.uid] = soco.boot_seqnum _LOGGER.debug("Adding new speaker: %s", speaker_info) - speaker = SonosSpeaker(self.hass, soco, speaker_info) + speaker = SonosSpeaker(self.hass, soco, speaker_info, zone_group_state_sub) self.data.discovered[soco.uid] = speaker for coordinator, coord_dict in ( (SonosAlarms, self.data.alarms), @@ -250,13 +324,25 @@ def _add_speaker(self, soco: SoCo) -> None: except (OSError, SoCoException): _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) - def _poll_manual_hosts(self, now: datetime.datetime | None = None) -> None: + async def async_poll_manual_hosts( + self, now: datetime.datetime | None = None + ) -> None: """Add and maintain Sonos devices from a manual configuration.""" + + def get_sync_attributes(soco: SoCo) -> set[SoCo]: + """Ensure I/O attributes are cached and return visible zones.""" + _ = soco.household_id + _ = soco.uid + return soco.visible_zones + for host in self.hosts: ip_addr = socket.gethostbyname(host) soco = SoCo(ip_addr) try: - visible_zones = soco.visible_zones + visible_zones = await self.hass.async_add_executor_job( + get_sync_attributes, + soco, + ) except OSError: _LOGGER.warning("Could not get visible Sonos devices from %s", ip_addr) else: @@ -267,7 +353,7 @@ def _poll_manual_hosts(self, now: datetime.datetime | None = None) -> None: }: _LOGGER.debug("Adding to manual hosts: %s", new_hosts) self.hosts.update(new_hosts) - dispatcher_send( + async_dispatcher_send( self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}", "manual zone scan", @@ -290,7 +376,9 @@ def _poll_manual_hosts(self, now: datetime.datetime | None = None) -> None: None, ) if not known_speaker: - self._create_visible_speakers(ip_addr) + await self._async_handle_discovery_message( + soco.uid, ip_addr, "manual zone scan" + ) elif not known_speaker.available: try: known_speaker.ping() @@ -299,33 +387,32 @@ def _poll_manual_hosts(self, now: datetime.datetime | None = None) -> None: "Manual poll to %s failed, keeping unavailable", ip_addr ) - self.data.hosts_heartbeat = call_later( - self.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts + self.data.hosts_heartbeat = async_call_later( + self.hass, DISCOVERY_INTERVAL.total_seconds(), self.async_poll_manual_hosts ) async def _async_handle_discovery_message( - self, uid: str, discovered_ip: str, boot_seqnum: int | None + self, + uid: str, + discovered_ip: str, + source: str, + boot_seqnum: int | None = None, ) -> None: """Handle discovered player creation and activity.""" async with self.discovery_lock: if not self.data.discovered: # Initial discovery, attempt to add all visible zones - await self.hass.async_add_executor_job( - self._create_visible_speakers, - discovered_ip, - ) + await self.async_subscribe_to_zone_updates(discovered_ip) elif uid not in self.data.discovered: if self.is_device_invisible(discovered_ip): return - await self.hass.async_add_executor_job( - self._add_speaker, SoCo(discovered_ip) - ) + await self.async_subscribe_to_zone_updates(discovered_ip) elif boot_seqnum and boot_seqnum > self.data.boot_counts[uid]: self.data.boot_counts[uid] = boot_seqnum async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}") else: async_dispatcher_send( - self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", "discovery" + self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", source ) async def _async_ssdp_discovered_player( @@ -389,7 +476,10 @@ def async_discovered_player( self.data.discovery_known.add(uid) asyncio.create_task( self._async_handle_discovery_message( - uid, discovered_ip, cast(Optional[int], boot_seqnum) + uid, + discovered_ip, + "discovery", + boot_seqnum=cast(Optional[int], boot_seqnum), ) ) @@ -408,7 +498,7 @@ async def setup_platforms_and_discovery(self) -> None: EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat ) ) - await self.hass.async_add_executor_job(self._poll_manual_hosts) + await self.async_poll_manual_hosts() self.entry.async_on_unload( await ssdp.async_register_callback( diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 5460230b66f0bf..b1dfac7beed8b4 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -69,14 +69,14 @@ "CHARGING": True, "NOT_CHARGING": False, } -SUBSCRIPTION_SERVICES = [ +SUBSCRIPTION_SERVICES = { "alarmClock", "avTransport", "contentDirectory", "deviceProperties", "renderingControl", "zoneGroupTopology", -] +} SUPPORTED_VANISH_REASONS = ("sleeping", "switch to bluetooth", "upgrade") UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] @@ -88,7 +88,11 @@ class SonosSpeaker: """Representation of a Sonos speaker.""" def __init__( - self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any] + self, + hass: HomeAssistant, + soco: SoCo, + speaker_info: dict[str, Any], + zone_group_state_sub: SubscriptionBase | None, ) -> None: """Initialize a SonosSpeaker.""" self.hass = hass @@ -112,6 +116,9 @@ def __init__( # Subscriptions and events self.subscriptions_failed: bool = False self._subscriptions: list[SubscriptionBase] = [] + if zone_group_state_sub: + zone_group_state_sub.callback = self.async_dispatch_event + self._subscriptions.append(zone_group_state_sub) self._subscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} self._last_activity: float = NEVER_TIME @@ -289,6 +296,12 @@ def subscription_address(self) -> str: addr, port = self._subscriptions[0].event_listener.address return ":".join([addr, str(port)]) + @property + def missing_subscriptions(self) -> set[str]: + """Return a list of missing service subscriptions.""" + subscribed_services = {sub.service.service_type for sub in self._subscriptions} + return SUBSCRIPTION_SERVICES - subscribed_services + # # Subscription handling and event dispatchers # @@ -321,8 +334,6 @@ async def async_subscribe(self) -> None: self._subscription_lock = asyncio.Lock() async with self._subscription_lock: - if self._subscriptions: - return try: await self._async_subscribe() except SonosSubscriptionsFailed: @@ -331,12 +342,14 @@ async def async_subscribe(self) -> None: async def _async_subscribe(self) -> None: """Create event subscriptions.""" - _LOGGER.debug("Creating subscriptions for %s", self.zone_name) - subscriptions = [ self._subscribe(getattr(self.soco, service), self.async_dispatch_event) - for service in SUBSCRIPTION_SERVICES + for service in self.missing_subscriptions ] + if not subscriptions: + return + + _LOGGER.debug("Creating subscriptions for %s", self.zone_name) results = await asyncio.gather(*subscriptions, return_exceptions=True) for result in results: self.log_subscription_result( diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 2ac1cb460cb80f..ef420c11ef243f 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,7 +10,7 @@ from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture class SonosMockService: @@ -66,13 +66,14 @@ async def async_autosetup_sonos(async_setup_sonos): @pytest.fixture -def async_setup_sonos(hass, config_entry): +def async_setup_sonos(hass, config_entry, fire_zgs_event): """Return a coroutine to set up a Sonos integration instance on demand.""" async def _wrapper(): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + await fire_zgs_event() return _wrapper @@ -349,3 +350,24 @@ def tv_event_fixture(soco): def mock_get_source_ip(mock_get_source_ip): """Mock network util's async_get_source_ip in all sonos tests.""" return mock_get_source_ip + + +@pytest.fixture(name="zgs_discovery", scope="session") +def zgs_discovery_fixture(): + """Load ZoneGroupState discovery payload and return it.""" + return load_fixture("sonos/zgs_discovery.xml") + + +@pytest.fixture(name="fire_zgs_event") +def zgs_event_fixture(hass, soco, zgs_discovery): + """Create alarm_event fixture.""" + variables = {"ZoneGroupState": zgs_discovery} + + async def _wrapper(): + event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) + subscription = soco.zoneGroupTopology.subscribe.return_value + sub_callback = subscription.callback + sub_callback(event) + await hass.async_block_till_done() + + return _wrapper diff --git a/tests/components/sonos/fixtures/zgs_discovery.xml b/tests/components/sonos/fixtures/zgs_discovery.xml new file mode 100644 index 00000000000000..3433bc0f32f114 --- /dev/null +++ b/tests/components/sonos/fixtures/zgs_discovery.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index f0e6c81a41131a..ebb8e0234e0fb4 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -62,8 +62,12 @@ async def test_user_form( async def test_user_form_already_created(hass: core.HomeAssistant): """Ensure we abort a flow if the entry is already created from config.""" config = {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: "192.168.4.2"}}} - await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + with patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 49ddbffc41accd..6c79cdc7367304 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -184,7 +184,7 @@ async def test_microphone_binary_sensor( assert mic_binary_sensor_state.state == STATE_ON -async def test_favorites_sensor(hass, async_autosetup_sonos, soco): +async def test_favorites_sensor(hass, async_autosetup_sonos, soco, fire_zgs_event): """Test Sonos favorites sensor.""" entity_registry = ent_reg.async_get(hass) favorites = entity_registry.entities["sensor.sonos_favorites"] @@ -208,6 +208,9 @@ async def test_favorites_sensor(hass, async_autosetup_sonos, soco): ) await hass.async_block_till_done() + # Trigger subscription callback for speaker discovery + await fire_zgs_event() + favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} ) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 2b794657565705..8b3fed989022d9 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -37,7 +37,7 @@ async def test_entity_registry(hass, async_autosetup_sonos): assert "switch.zone_a_touch_controls" in entity_registry.entities -async def test_switch_attributes(hass, async_autosetup_sonos, soco): +async def test_switch_attributes(hass, async_autosetup_sonos, soco, fire_zgs_event): """Test for correct Sonos switch states.""" entity_registry = ent_reg.async_get(hass) @@ -114,6 +114,9 @@ async def test_switch_attributes(hass, async_autosetup_sonos, soco): await hass.async_block_till_done() assert m.called + # Trigger subscription callback for speaker discovery + await fire_zgs_event() + status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON From 90a69242196ffb1a4f3436d46e69b7a36609b188 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 8 Jan 2023 22:07:10 +0100 Subject: [PATCH 07/35] Code styling tweaks to the MQTT integration (#85463) --- homeassistant/components/mqtt/__init__.py | 9 ++- .../components/mqtt/alarm_control_panel.py | 5 +- .../components/mqtt/binary_sensor.py | 5 +- homeassistant/components/mqtt/button.py | 5 +- homeassistant/components/mqtt/camera.py | 2 +- homeassistant/components/mqtt/client.py | 3 +- homeassistant/components/mqtt/climate.py | 31 ++++++---- homeassistant/components/mqtt/config_flow.py | 3 +- .../components/mqtt/config_integration.py | 60 ++++++++++++------- homeassistant/components/mqtt/cover.py | 5 +- .../components/mqtt/device_tracker.py | 5 +- homeassistant/components/mqtt/fan.py | 5 +- homeassistant/components/mqtt/humidifier.py | 11 +++- homeassistant/components/mqtt/lock.py | 2 +- homeassistant/components/mqtt/mixins.py | 32 +++++++--- homeassistant/components/mqtt/number.py | 2 +- homeassistant/components/mqtt/scene.py | 5 +- homeassistant/components/mqtt/select.py | 2 +- homeassistant/components/mqtt/sensor.py | 8 ++- homeassistant/components/mqtt/siren.py | 6 +- homeassistant/components/mqtt/switch.py | 5 +- homeassistant/components/mqtt/text.py | 2 +- homeassistant/components/mqtt/update.py | 2 +- .../components/mqtt/vacuum/__init__.py | 8 ++- .../components/mqtt/vacuum/schema_legacy.py | 6 +- .../components/mqtt/vacuum/schema_state.py | 3 +- 26 files changed, 152 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3f6f212275732e..066f3be37367f8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -486,7 +486,8 @@ async def _reload_config(call: ServiceCall) -> None: entity.async_remove() for mqtt_platform in mqtt_platforms for entity in mqtt_platform.entities.values() - if not entity._discovery_data # type: ignore[attr-defined] # pylint: disable=protected-access + # pylint: disable=protected-access + if not entity._discovery_data # type: ignore[attr-defined] if mqtt_platform.config_entry and mqtt_platform.domain in RELOADABLE_PLATFORMS ] @@ -542,7 +543,8 @@ async def async_forward_entry_setup_and_setup_discovery( mqtt_data.reload_entry = False reload_manual_setup = True - # When the entry was disabled before, reload manual set up items to enable MQTT again + # When the entry was disabled before, reload manual set up items to enable + # MQTT again if mqtt_data.reload_needed: mqtt_data.reload_needed = False reload_manual_setup = True @@ -710,7 +712,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Trigger reload manual MQTT items at entry setup if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: - # The entry is disabled reload legacy manual items when the entry is enabled again + # The entry is disabled reload legacy manual items when + # the entry is enabled again mqtt_data.reload_needed = True elif mqtt_entry_status is True: # The entry is reloaded: diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index aa796d9ea8f666..8651311328173e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -112,7 +112,8 @@ } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT alarm control panels under the alarm_control_panel platform key was deprecated in HA Core 2022.6 +# Configuring MQTT alarm control panels under the alarm_control_panel platform key +# was deprecated in HA Core 2022.6; # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(alarm.DOMAIN), @@ -126,7 +127,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT alarm control panel through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 5ed9fdfb76f95c..135151f179cd47 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -69,7 +69,8 @@ } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Binary sensors under the binary_sensor platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Binary sensors under the binary_sensor platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(binary_sensor.DOMAIN), @@ -83,7 +84,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT binary sensor through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index d50a06a46d83c6..f81f78a487a0e2 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -46,7 +46,8 @@ } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Buttons under the button platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Buttons under the button platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(button.DOMAIN), @@ -61,7 +62,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT button through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 6ece232775aab3..b3a78f4d2ffe8c 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -70,7 +70,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT camera through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7dc6048f2f7080..aa30c6c18af951 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -132,7 +132,8 @@ async def async_publish( return outgoing_payload = str(payload) if encoding != DEFAULT_ENCODING: - # a string is encoded as utf-8 by default, other encoding requires bytes as payload + # A string is encoded as utf-8 by default, other encoding + # requires bytes as payload try: outgoing_payload = outgoing_payload.encode(encoding) except (AttributeError, LookupError, UnicodeEncodeError): diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 99b51d2ab3ebf0..b64e5ed08c3ba7 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -82,7 +82,8 @@ CONF_AUX_COMMAND_TOPIC = "aux_command_topic" CONF_AUX_STATE_TEMPLATE = "aux_state_template" CONF_AUX_STATE_TOPIC = "aux_state_topic" -# AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, +# support was removed with release 2022.9 CONF_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic" CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template" CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic" @@ -96,7 +97,8 @@ CONF_FAN_MODE_LIST = "fan_modes" CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" -# AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, +# support was removed with release 2022.9 CONF_HOLD_COMMAND_TEMPLATE = "hold_command_template" CONF_HOLD_COMMAND_TOPIC = "hold_command_topic" CONF_HOLD_STATE_TEMPLATE = "hold_state_template" @@ -235,7 +237,7 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: - """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + """Validate a target_humidity range configuration, throws otherwise.""" if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]: raise ValueError("target_humidity_max must be > target_humidity_min") if config[CONF_HUMIDITY_MAX] > 100: @@ -245,13 +247,18 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: - """Validate that if CONF_HUMIDITY_STATE_TOPIC is set then CONF_HUMIDITY_COMMAND_TOPIC is also set.""" + """Validate humidity state. + + Ensure that if CONF_HUMIDITY_STATE_TOPIC is set then + CONF_HUMIDITY_COMMAND_TOPIC is also set. + """ if ( CONF_HUMIDITY_STATE_TOPIC in config and CONF_HUMIDITY_COMMAND_TOPIC not in config ): raise ValueError( - f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without {CONF_HUMIDITY_COMMAND_TOPIC}" + f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without" + f" {CONF_HUMIDITY_COMMAND_TOPIC}" ) return config @@ -312,7 +319,8 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic, - # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST + # must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" ): valid_publish_topic, @@ -353,7 +361,8 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: PLATFORM_SCHEMA_MODERN = vol.All( # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + # AWAY and HOLD mode topics and templates are no longer supported, + # support was removed with release 2022.9 cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), cv.removed(CONF_AWAY_MODE_STATE_TOPIC), @@ -368,7 +377,8 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: valid_humidity_state_configuration, ) -# Configuring MQTT Climate under the climate platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Climate under the climate platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(climate.DOMAIN), @@ -380,7 +390,8 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: _DISCOVERY_SCHEMA_BASE, # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + # AWAY and HOLD mode topics and templates are no longer supported, + # support was removed with release 2022.9 cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), cv.removed(CONF_AWAY_MODE_STATE_TOPIC), @@ -400,7 +411,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT climate device through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index b79ff30f1118a4..168f8b71cdee98 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -589,7 +589,8 @@ async def _async_validate_broker_settings( current_user = current_config.get(CONF_USERNAME) current_pass = current_config.get(CONF_PASSWORD) - # Treat the previous post as an update of the current settings (if there was a basic broker setup step) + # Treat the previous post as an update of the current settings + # (if there was a basic broker setup step) current_config.update(user_input_basic) # Get default settings for advanced broker options diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 4140c5963cafc6..bbd6861435bba3 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -81,64 +81,84 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( { Platform.ALARM_CONTROL_PANEL.value: vol.All( - cv.ensure_list, [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] # noqa: E501 ), Platform.BINARY_SENSOR.value: vol.All( - cv.ensure_list, [binary_sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [binary_sensor_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.BUTTON.value: vol.All( - cv.ensure_list, [button_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [button_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.CAMERA.value: vol.All( - cv.ensure_list, [camera_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [camera_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.CLIMATE.value: vol.All( - cv.ensure_list, [climate_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [climate_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.COVER.value: vol.All( - cv.ensure_list, [cover_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [cover_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.DEVICE_TRACKER.value: vol.All( - cv.ensure_list, [device_tracker_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [device_tracker_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.FAN.value: vol.All( - cv.ensure_list, [fan_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [fan_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.HUMIDIFIER.value: vol.All( - cv.ensure_list, [humidifier_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [humidifier_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.LOCK.value: vol.All( - cv.ensure_list, [lock_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [lock_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.LIGHT.value: vol.All( - cv.ensure_list, [light_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [light_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.NUMBER.value: vol.All( - cv.ensure_list, [number_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [number_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SCENE.value: vol.All( - cv.ensure_list, [scene_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [scene_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SELECT.value: vol.All( - cv.ensure_list, [select_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [select_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SENSOR.value: vol.All( - cv.ensure_list, [sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [sensor_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SIREN.value: vol.All( - cv.ensure_list, [siren_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [siren_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SWITCH.value: vol.All( - cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [switch_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.TEXT.value: vol.All( - cv.ensure_list, [text_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [text_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.UPDATE.value: vol.All( - cv.ensure_list, [update_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [update_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.VACUUM.value: vol.All( - cv.ensure_list, [vacuum_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [vacuum_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), } ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index f0733af8bc39e5..66b8e60b561c31 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -227,7 +227,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT cover through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -656,7 +656,8 @@ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: tilt = kwargs[ATTR_TILT_POSITION] percentage_tilt = tilt tilt = self.find_in_range_from_percent(tilt) - # Handover the tilt after calculated from percent would make it more consistent with receiving templates + # Handover the tilt after calculated from percent would make it more + # consistent with receiving templates variables = { "tilt_position": percentage_tilt, "entity_id": self.entity_id, diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index 92f213f4bdf392..b55c3754696107 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -61,7 +61,8 @@ DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -# Configuring MQTT Device Trackers under the device_tracker platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Device Trackers under the device_tracker platform key was deprecated +# in HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All(warn_for_legacy_schema(device_tracker.DOMAIN)) @@ -71,7 +72,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT device_tracker through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 61fbc4fd387719..74290abb7576e3 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -136,7 +136,8 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template, - # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST + # must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" ): valid_publish_topic, @@ -194,7 +195,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT fan through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index ab51b0b9b4571e..93069791a797d2 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -101,7 +101,11 @@ def valid_mode_configuration(config: ConfigType) -> ConfigType: def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: - """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + """Validate humidity range. + + Ensures that the target_humidity range configuration is valid, + throws if it isn't. + """ if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]: raise ValueError("target_humidity_max must be > target_humidity_min") if config[CONF_TARGET_HUMIDITY_MAX] > 100: @@ -147,7 +151,8 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Humidifiers under the humidifier platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Humidifiers under the humidifier platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(humidifier.DOMAIN), @@ -171,7 +176,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT humidifier through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index a518300b7f0d85..f56dba6766abca 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -82,7 +82,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT lock through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 1e6e9989577daf..1487053bbdac37 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -264,7 +264,10 @@ def validator(config: ConfigType) -> ConfigType: severity=IssueSeverity.ERROR, translation_key="deprecated_yaml", translation_placeholders={ - "more_info_url": f"https://www.home-assistant.io/integrations/{domain}.mqtt/#new_format", + "more_info_url": ( + "https://www.home-assistant.io" + f"/integrations/{domain}.mqtt/#new_format" + ), "platform": domain, }, ) @@ -600,7 +603,11 @@ def available(self) -> bool: async def cleanup_device_registry( hass: HomeAssistant, device_id: str | None, config_entry_id: str | None ) -> None: - """Remove MQTT from the device registry entry if there are no remaining entities, triggers or tags.""" + """Clean up the device registry after MQTT removal. + + Remove MQTT from the device registry entry if there are no remaining + entities, triggers or tags. + """ # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import device_trigger, tag @@ -649,7 +656,11 @@ def stop_discovery_updates( async def async_remove_discovery_payload( hass: HomeAssistant, discovery_data: DiscoveryInfoType ) -> None: - """Clear retained discovery topic in broker to avoid rediscovery after a restart of HA.""" + """Clear retained discovery payload. + + Remove discovery topic in broker to avoid rediscovery + after a restart of Home Assistant. + """ discovery_topic = discovery_data[ATTR_DISCOVERY_TOPIC] await async_publish(hass, discovery_topic, "", retain=True) @@ -829,8 +840,9 @@ async def _async_remove_state_and_registry_entry( ) -> None: """Remove entity's state and entity registry entry. - Remove entity from entity registry if it is registered, this also removes the state. - If the entity is not in the entity registry, just remove the state. + Remove entity from entity registry if it is registered, + this also removes the state. If the entity is not in the entity + registry, just remove the state. """ entity_registry = er.async_get(self.hass) if entity_entry := entity_registry.async_get(self.entity_id): @@ -872,7 +884,8 @@ async def discovery_callback(payload: MQTTDiscoveryPayload) -> None: debug_info.add_entity_discovery_data( self.hass, self._discovery_data, self.entity_id ) - # Set in case the entity has been removed and is re-added, for example when changing entity_id + # Set in case the entity has been removed and is re-added, + # for example when changing entity_id set_discovery_hash(self.hass, discovery_hash) self._remove_discovery_updated = async_dispatcher_connect( self.hass, @@ -883,11 +896,12 @@ async def discovery_callback(payload: MQTTDiscoveryPayload) -> None: async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" if not self._removed_from_hass and self._discovery_data is not None: - # Stop subscribing to discovery updates to not trigger when we clear the - # discovery topic + # Stop subscribing to discovery updates to not trigger when we + # clear the discovery topic self._cleanup_discovery_on_remove() - # Clear the discovery topic so the entity is not rediscovered after a restart + # Clear the discovery topic so the entity is not + # rediscovered after a restart await async_remove_discovery_payload(self.hass, self._discovery_data) @callback diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index cef9f47f0d9413..3682b19cf4ec71 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -122,7 +122,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT number through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 3454102e5e03dc..dd7f3347845f91 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -40,7 +40,8 @@ vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_OBJECT_ID): cv.string, - # CONF_ENABLED_BY_DEFAULT is not added by default because we are not using the common schema here + # CONF_ENABLED_BY_DEFAULT is not added by default because + # we are not using the common schema here vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) @@ -59,7 +60,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT scene through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 6d07a1a5fff884..b783a001f15017 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -78,7 +78,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT select through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index dbb414921b5d59..656de35232b23d 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -114,7 +114,8 @@ def validate_options(conf: ConfigType) -> ConfigType: validate_options, ) -# Configuring MQTT Sensors under the sensor platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Sensors under the sensor platform key was deprecated in +# HA Core 2022.6 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(sensor.DOMAIN), ) @@ -131,7 +132,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT sensor through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -248,7 +249,8 @@ def _prepare_subscribe_topics(self) -> None: def _update_state(msg: ReceiveMessage) -> None: # auto-expire enabled? if self._expire_after is not None and self._expire_after > 0: - # When self._expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + # When self._expire_after is set, and we receive a message, assume + # device is not expired since it has to be to receive the message self._expired = False # Reset old trigger diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 13ccfdc9cb2453..b1ec05aefa3c89 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -128,7 +128,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT siren through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -386,4 +386,6 @@ def _update(self, data: SirenTurnOnServiceParameters) -> None: """Update the extra siren state attributes.""" for attribute, support in SUPPORTED_ATTRIBUTES.items(): if self._attr_supported_features & support and attribute in data: - self._attr_extra_state_attributes[attribute] = data[attribute] # type: ignore[literal-required] + self._attr_extra_state_attributes[attribute] = data[ + attribute # type: ignore[literal-required] + ] diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f3fcf30c7ea151..521b08d27489ab 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -64,7 +64,8 @@ } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Switches under the switch platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Switches under the switch platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(switch.DOMAIN), @@ -78,7 +79,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT switch through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 824aeb2f4c5de3..cc05a2d8db16c4 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -102,7 +102,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT text through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT text through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 9ee0690b120651..f0158be11d7ba6 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -76,7 +76,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT update through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT update through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 60f8d7a7d45467..366a7dca159659 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -34,7 +34,8 @@ def validate_mqtt_vacuum_discovery(config_value: ConfigType) -> ConfigType: return config -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 def validate_mqtt_vacuum(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum schema (deprecated).""" schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} @@ -56,7 +57,8 @@ def validate_mqtt_vacuum_modern(config_value: ConfigType) -> ConfigType: MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_discovery ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(vacuum.DOMAIN), @@ -72,7 +74,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT vacuum through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 94053e4fb72173..39b734235a0140 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -160,7 +160,8 @@ .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 PLATFORM_SCHEMA_LEGACY = vol.All( cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_LEGACY_MODERN.schema), warn_for_legacy_schema(VACUUM_DOMAIN), @@ -413,7 +414,8 @@ async def _subscribe_topics(self) -> None: def battery_icon(self) -> str: """Return the battery icon for the vacuum cleaner. - No need to check VacuumEntityFeature.BATTERY, this won't be called if battery_level is None. + No need to check VacuumEntityFeature.BATTERY, this won't be called if + battery_level is None. """ return icon_for_battery_level( battery_level=self.battery_level, charging=self._charging diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 210e189c2fc34c..3a5d267a5d4c89 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -154,7 +154,8 @@ .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 PLATFORM_SCHEMA_STATE = vol.All( cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_STATE_MODERN.schema), warn_for_legacy_schema(VACUUM_DOMAIN), From dd00ee35fe58245aa8edd764e98552b5631b38c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 8 Jan 2023 22:20:02 +0100 Subject: [PATCH 08/35] Code styling tweaks to Bluetooth (#85448) Co-authored-by: J. Nick Koston --- .../bluetooth/active_update_coordinator.py | 20 ++++++---- .../bluetooth/active_update_processor.py | 25 ++++++++----- .../components/bluetooth/base_scanner.py | 9 +++-- homeassistant/components/bluetooth/manager.py | 37 +++++++++++-------- homeassistant/components/bluetooth/match.py | 5 ++- homeassistant/components/bluetooth/usage.py | 11 ++++-- homeassistant/components/bluetooth/util.py | 5 ++- .../components/bluetooth/wrappers.py | 25 ++++++++----- 8 files changed, 87 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 5371d9f99faa2f..09567aada05cf5 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -1,4 +1,7 @@ -"""A Bluetooth passive coordinator that receives data from advertisements but can also poll.""" +"""A Bluetooth passive coordinator. + +Receives data from advertisements but can also poll. +""" from __future__ import annotations from collections.abc import Callable, Coroutine @@ -33,16 +36,19 @@ class ActiveBluetoothDataUpdateCoordinator( out if a poll is needed. This should return True if it is and False if it is not needed. - def needs_poll_method(svc_info: BluetoothServiceInfoBleak, last_poll: float | None) -> bool: + def needs_poll_method( + svc_info: BluetoothServiceInfoBleak, + last_poll: float | None + ) -> bool: return True - If there has been no poll since HA started, `last_poll` will be None. Otherwise it is - the number of seconds since one was last attempted. + If there has been no poll since HA started, `last_poll` will be None. + Otherwise it is the number of seconds since one was last attempted. If a poll is needed, the coordinator will call poll_method. This is a coroutine. - It should return the same type of data as your update_method. The expectation is that - data from advertisements and from polling are being parsed and fed into a shared - object that represents the current state of the device. + It should return the same type of data as your update_method. The expectation is + that data from advertisements and from polling are being parsed and fed into + a shared object that represents the current state of the device. async def poll_method(svc_info: BluetoothServiceInfoBleak) -> YourDataType: return YourDataType(....) diff --git a/homeassistant/components/bluetooth/active_update_processor.py b/homeassistant/components/bluetooth/active_update_processor.py index e175fc665f4510..b91ac2cbf4d6fc 100644 --- a/homeassistant/components/bluetooth/active_update_processor.py +++ b/homeassistant/components/bluetooth/active_update_processor.py @@ -1,4 +1,7 @@ -"""A Bluetooth passive processor coordinator that collects data from advertisements but can also poll.""" +"""A Bluetooth passive processor coordinator. + +Collects data from advertisements but can also poll. +""" from __future__ import annotations from collections.abc import Callable, Coroutine @@ -23,23 +26,27 @@ class ActiveBluetoothProcessorCoordinator( Generic[_T], PassiveBluetoothProcessorCoordinator[_T] ): - """ - A processor coordinator that parses passive data from advertisements but can also poll. + """A processor coordinator that parses passive data. + + Parses passive data from advertisements but can also poll. Every time an advertisement is received, needs_poll_method is called to work out if a poll is needed. This should return True if it is and False if it is not needed. - def needs_poll_method(svc_info: BluetoothServiceInfoBleak, last_poll: float | None) -> bool: + def needs_poll_method( + svc_info: BluetoothServiceInfoBleak, + last_poll: float | None + ) -> bool: return True - If there has been no poll since HA started, `last_poll` will be None. Otherwise it is - the number of seconds since one was last attempted. + If there has been no poll since HA started, `last_poll` will be None. + Otherwise it is the number of seconds since one was last attempted. If a poll is needed, the coordinator will call poll_method. This is a coroutine. - It should return the same type of data as your update_method. The expectation is that - data from advertisements and from polling are being parsed and fed into a shared - object that represents the current state of the device. + It should return the same type of data as your update_method. The expectation is + that data from advertisements and from polling are being parsed and fed into a + shared object that represents the current state of the device. async def poll_method(svc_info: BluetoothServiceInfoBleak) -> YourDataType: return YourDataType(....) diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index b4c88260591a91..8868b0a0883487 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -107,7 +107,8 @@ def _async_watchdog_triggered(self) -> bool: def _async_scanner_watchdog(self, now: datetime.datetime) -> None: """Check if the scanner is running. - Override this method if you need to do something else when the watchdog is triggered. + Override this method if you need to do something else when the watchdog + is triggered. """ if self._async_watchdog_triggered(): _LOGGER.info( @@ -144,6 +145,7 @@ def discovered_devices_and_advertisement_data( async def async_diagnostics(self) -> dict[str, Any]: """Return diagnostic information about the scanner.""" + device_adv_datas = self.discovered_devices_and_advertisement_data.values() return { "name": self.name, "start_time": self._start_time, @@ -160,7 +162,7 @@ async def async_diagnostics(self) -> dict[str, Any]: "advertisement_data": device_adv[1], "details": device_adv[0].details, } - for device_adv in self.discovered_devices_and_advertisement_data.values() + for device_adv in device_adv_datas ], } @@ -258,9 +260,10 @@ def _async_expire_devices(self, _datetime: datetime.datetime) -> None: @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" + device_adv_datas = self._discovered_device_advertisement_datas.values() return [ device_advertisement_data[0] - for device_advertisement_data in self._discovered_device_advertisement_datas.values() + for device_advertisement_data in device_adv_datas ] @property diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 748b685d866b38..c863299d206426 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -225,15 +225,17 @@ def async_get_scanner_discovered_devices_and_advertisement_data_by_address( results: list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]] = [] for type_ in types_: for scanner in self._get_scanners_by_type(type_): - if device_advertisement_data := scanner.discovered_devices_and_advertisement_data.get( - address - ): - results.append((scanner, *device_advertisement_data)) + devices_and_adv_data = scanner.discovered_devices_and_advertisement_data + if device_adv_data := devices_and_adv_data.get(address): + results.append((scanner, *device_adv_data)) return results @hass_callback def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]: - """Return all of discovered addresses from all the scanners including duplicates.""" + """Return all of discovered addresses. + + Include addresses from all the scanners including duplicates. + """ yield from itertools.chain.from_iterable( scanner.discovered_devices_and_advertisement_data for scanner in self._get_scanners_by_type(True) @@ -281,9 +283,9 @@ def _async_check_unavailable(self, now: datetime) -> None: # # For non-connectable devices we also check the device has exceeded # the advertising interval before we mark it as unavailable - # since it may have gone to sleep and since we do not need an active connection - # to it we can only determine its availability by the lack of advertisements - # + # since it may have gone to sleep and since we do not need an active + # connection to it we can only determine its availability + # by the lack of advertisements if advertising_interval := intervals.get(address): time_since_seen = monotonic_now - all_history[address].time if time_since_seen <= advertising_interval: @@ -335,7 +337,8 @@ def _prefer_previous_adv_from_different_source( if (new.rssi or NO_RSSI_VALUE) - RSSI_SWITCH_THRESHOLD > ( old.rssi or NO_RSSI_VALUE ): - # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred + # If new advertisement is RSSI_SWITCH_THRESHOLD more, + # the new one is preferred. if debug: _LOGGER.debug( ( @@ -381,19 +384,21 @@ def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: source = service_info.source debug = _LOGGER.isEnabledFor(logging.DEBUG) - # This logic is complex due to the many combinations of scanners that are supported. + # This logic is complex due to the many combinations of scanners + # that are supported. # # We need to handle multiple connectable and non-connectable scanners # and we need to handle the case where a device is connectable on one scanner # but not on another. # - # The device may also be connectable only by a scanner that has worse signal strength - # than a non-connectable scanner. + # The device may also be connectable only by a scanner that has worse + # signal strength than a non-connectable scanner. # - # all_history - the history of all advertisements from all scanners with the best - # advertisement from each scanner - # connectable_history - the history of all connectable advertisements from all scanners - # with the best advertisement from each connectable scanner + # all_history - the history of all advertisements from all scanners with the + # best advertisement from each scanner + # connectable_history - the history of all connectable advertisements from all + # scanners with the best advertisement from each + # connectable scanner # if ( (old_service_info := all_history.get(address)) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 1a59ee6fe4c42d..a7308bfd7ff5a3 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -282,7 +282,10 @@ class BluetoothMatcherIndex(BluetoothMatcherIndexBase[BluetoothMatcher]): class BluetoothCallbackMatcherIndex( BluetoothMatcherIndexBase[BluetoothCallbackMatcherWithCallback] ): - """Bluetooth matcher for the bluetooth integration that supports matching on addresses.""" + """Bluetooth matcher for the bluetooth integration. + + Supports matching on addresses. + """ def __init__(self) -> None: """Initialize the matcher index.""" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index 0b1e615dddab72..b751559e7a4418 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -16,17 +16,22 @@ def install_multiple_bleak_catcher() -> None: - """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" + """Wrap the bleak classes to return the shared instance. + + In case multiple instances are detected. + """ bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] - bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] + bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] # noqa: E501 def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] bleak.BleakClient = ORIGINAL_BLEAK_CLIENT # type: ignore[misc] - bleak_retry_connector.BleakClientWithServiceCache = ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT # type: ignore[misc] + bleak_retry_connector.BleakClientWithServiceCache = ( # type: ignore[misc] + ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT + ) class HaBleakClientWithServiceCache(HaBleakClientWrapper): diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 5419fa79e1cb15..e78eb51a38c115 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -15,7 +15,10 @@ def async_load_history_from_system( adapters: BluetoothAdapters, storage: BluetoothStorage ) -> tuple[dict[str, BluetoothServiceInfoBleak], dict[str, BluetoothServiceInfoBleak]]: - """Load the device and advertisement_data history if available on the current system.""" + """Load the device and advertisement_data history. + + Only loads if available on the current system. + """ now_monotonic = monotonic_time_coarse() connectable_loaded_history: dict[str, BluetoothServiceInfoBleak] = {} all_loaded_history: dict[str, BluetoothServiceInfoBleak] = {} diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index a2c417ca38259c..4a1be63903fc2a 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -119,10 +119,11 @@ def discovered_devices(self) -> list[BLEDevice]: def register_detection_callback( self, callback: AdvertisementDataCallback | None ) -> None: - """Register a callback that is called when a device is discovered or has a property changed. + """Register a detection callback. - This method takes the callback and registers it with the long running - scanner. + The callback is called when a device is discovered or has a property changed. + + This method takes the callback and registers it with the long running sscanner. """ self._advertisement_data_callback = callback self._setup_detection_callback() @@ -154,7 +155,9 @@ def _rssi_sorter_with_connection_failure_penalty( connection_failure_count: dict[BaseHaScanner, int], rssi_diff: int, ) -> float: - """Get a sorted list of scanner, device, advertisement data adjusting for previous connection failures. + """Get a sorted list of scanner, device, advertisement data. + + Adjusting for previous connection failures. When a connection fails, we want to try the next best adapter so we apply a penalty to the RSSI value to make it less likely to be chosen @@ -227,7 +230,10 @@ def set_disconnected_callback( """Set the disconnect callback.""" self.__disconnected_callback = callback if self._backend: - self._backend.set_disconnected_callback(callback, **kwargs) # type: ignore[arg-type] + self._backend.set_disconnected_callback( + callback, # type: ignore[arg-type] + **kwargs, + ) async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" @@ -294,15 +300,14 @@ def _async_get_best_available_backend_and_device( that has a free connection slot. """ address = self.__address - scanner_device_advertisement_datas = manager.async_get_scanner_discovered_devices_and_advertisement_data_by_address( + scanner_device_advertisement_datas = manager.async_get_scanner_discovered_devices_and_advertisement_data_by_address( # noqa: E501 address, True ) sorted_scanner_device_advertisement_datas = sorted( scanner_device_advertisement_datas, - key=lambda scanner_device_advertisement_data: scanner_device_advertisement_data[ - 2 - ].rssi - or NO_RSSI_VALUE, + key=lambda scanner_device_advertisement_data: ( + scanner_device_advertisement_data[2].rssi or NO_RSSI_VALUE + ), reverse=True, ) From 01ce7a16410db12007f047cfd532dc46ea4fa832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 9 Jan 2023 00:52:05 +0200 Subject: [PATCH 09/35] Address a few deprecation warnings in tests (#85472) --- tests/conftest.py | 2 +- tests/test_core.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 75655cf2d8682b..6cfd4d158232db 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -627,7 +627,7 @@ def current_request(): "GET", "/some/request", headers={"Host": "example.com"}, - sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS), + sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), ) mock_request_context.get.return_value = mocked_request yield mock_request_context diff --git a/tests/test_core.py b/tests/test_core.py index 2f8db7fc0d6d4e..d7ea89b2c41ff4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1128,7 +1128,12 @@ async def handle_outer(call): call2 = hass.services.async_call( "test", "inner", blocking=True, context=call.context ) - await asyncio.wait([call1, call2]) + await asyncio.wait( + [ + hass.async_create_task(call1), + hass.async_create_task(call2), + ] + ) calls.append(call) hass.services.async_register("test", "outer", handle_outer) From 8cf46bee0657a574d993e97acb4a9662067934ad Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 8 Jan 2023 16:52:22 -0600 Subject: [PATCH 10/35] Bump soco to 0.29.0 (#85473) --- homeassistant/components/sonos/__init__.py | 1 + homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 45b78cd0dd6b4d..1c9ffc0264780a 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -124,6 +124,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Sonos from a config entry.""" soco_config.EVENTS_MODULE = events_asyncio soco_config.REQUEST_TIMEOUT = 9.5 + soco_config.ZGT_EVENT_FALLBACK = False zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT if DATA_SONOS not in hass.data: diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 57438d1864a215..73ad2a46c5f727 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": ["soco==0.28.1"], + "requirements": ["soco==0.29.0"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index eec72027109de8..eb4c5ee1956000 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2342,7 +2342,7 @@ smhi-pkg==1.0.16 snapcast==2.3.0 # homeassistant.components.sonos -soco==0.28.1 +soco==0.29.0 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a1c463d2a7775..4159aa54f0405c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1636,7 +1636,7 @@ smart-meter-texas==0.4.7 smhi-pkg==1.0.16 # homeassistant.components.sonos -soco==0.28.1 +soco==0.29.0 # homeassistant.components.solaredge solaredge==0.0.2 From d94eb2f92ebc7c418037e806da57f7453f2e4fb1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 8 Jan 2023 23:53:17 +0100 Subject: [PATCH 11/35] Code styling tweaks to the AdGuard Home integration (#85468) --- homeassistant/components/adguard/entity.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py index 7d6bf0993668f6..3a60ad4e8b1316 100644 --- a/homeassistant/components/adguard/entity.py +++ b/homeassistant/components/adguard/entity.py @@ -58,7 +58,12 @@ def device_info(self) -> DeviceInfo: return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={ - (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] + ( # type: ignore[arg-type] + DOMAIN, + self.adguard.host, + self.adguard.port, + self.adguard.base_path, + ) }, manufacturer="AdGuard Team", name="AdGuard Home", From 6599e4a652feeec590e0d2aa7802ecf9068e8d0c Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 8 Jan 2023 23:57:44 +0100 Subject: [PATCH 12/35] Fix fetching of initial data of Netgear sensors (#85450) fix fetching of initial data --- homeassistant/components/netgear/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 8c6bf56efccded..a350bfe92656fc 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -432,7 +432,7 @@ async def async_added_to_hass(self) -> None: if sensor_data is not None: self._value = sensor_data.native_value else: - self.schedule_update_ha_state() + self.coordinator.async_request_refresh() @callback def async_update_device(self) -> None: From a375abc81d40fa38d73b9057519c3f023364114c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 8 Jan 2023 23:59:07 +0100 Subject: [PATCH 13/35] Update pydocstyle to 6.2.3 (#85449) --- .pre-commit-config.yaml | 2 +- .../components/device_automation/__init__.py | 12 ++++++------ requirements_test_pre_commit.txt | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d68fde0ec255fe..2b96684d55f4d9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: - pycodestyle==2.10.0 - pyflakes==3.0.1 - flake8-docstrings==1.6.0 - - pydocstyle==6.1.1 + - pydocstyle==6.2.3 - flake8-comprehensions==3.10.1 - flake8-noqa==1.3.0 - mccabe==0.7.0 diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 3b75f4fff4cb54..65aad7def74301 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -120,7 +120,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.TRIGGER], @@ -129,7 +129,7 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.CONDITION], @@ -138,7 +138,7 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.ACTION], @@ -147,15 +147,15 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType -) -> "DeviceAutomationPlatformType": +) -> DeviceAutomationPlatformType: ... async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType -) -> "DeviceAutomationPlatformType": +) -> DeviceAutomationPlatformType: """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index cb80f4544c2581..8644ae23a16911 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -11,7 +11,7 @@ flake8==6.0.0 isort==5.11.4 mccabe==0.7.0 pycodestyle==2.10.0 -pydocstyle==6.1.1 +pydocstyle==6.2.3 pyflakes==3.0.1 pyupgrade==3.3.1 yamllint==1.28.0 From 15db01453cd97fb28a5794929a0ef90805679db5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:03:05 +0100 Subject: [PATCH 14/35] Code styling tweaks to the WLED integration (#85466) --- homeassistant/components/wled/switch.py | 7 +++--- tests/components/wled/test_diagnostics.py | 29 ++++++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 9f241756e90ebb..4a36fededbe443 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -65,10 +65,11 @@ def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" + state = self.coordinator.data.state return { - ATTR_DURATION: self.coordinator.data.state.nightlight.duration, - ATTR_FADE: self.coordinator.data.state.nightlight.fade, - ATTR_TARGET_BRIGHTNESS: self.coordinator.data.state.nightlight.target_brightness, + ATTR_DURATION: state.nightlight.duration, + ATTR_FADE: state.nightlight.fade, + ATTR_TARGET_BRIGHTNESS: state.nightlight.target_brightness, } @property diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py index 8f086331f8f0a5..3588ecdb498b03 100644 --- a/tests/components/wled/test_diagnostics.py +++ b/tests/components/wled/test_diagnostics.py @@ -50,7 +50,10 @@ async def test_diagnostics( "brightness": 127, "nightlight": { "__type": "", - "repr": "Nightlight(duration=60, fade=True, on=False, mode=, target_brightness=0)", + "repr": ( + "Nightlight(duration=60, fade=True, on=False," + " mode=, target_brightness=0)" + ), }, "on": True, "playlist": -1, @@ -58,11 +61,31 @@ async def test_diagnostics( "segments": [ { "__type": "", - "repr": "Segment(brightness=127, clones=-1, color_primary=(255, 159, 0), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=0, name='Solid'), intensity=128, length=20, on=True, palette=Palette(name='Default', palette_id=0), reverse=False, segment_id=0, selected=True, speed=32, start=0, stop=19)", + "repr": ( + "Segment(brightness=127, clones=-1," + " color_primary=(255, 159, 0)," + " color_secondary=(0, 0, 0)," + " color_tertiary=(0, 0, 0)," + " effect=Effect(effect_id=0, name='Solid')," + " intensity=128, length=20, on=True," + " palette=Palette(name='Default', palette_id=0)," + " reverse=False, segment_id=0, selected=True," + " speed=32, start=0, stop=19)" + ), }, { "__type": "", - "repr": "Segment(brightness=127, clones=-1, color_primary=(0, 255, 123), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=1, name='Blink'), intensity=64, length=10, on=True, palette=Palette(name='Random Cycle', palette_id=1), reverse=True, segment_id=1, selected=True, speed=16, start=20, stop=30)", + "repr": ( + "Segment(brightness=127, clones=-1," + " color_primary=(0, 255, 123)," + " color_secondary=(0, 0, 0)," + " color_tertiary=(0, 0, 0)," + " effect=Effect(effect_id=1, name='Blink')," + " intensity=64, length=10, on=True," + " palette=Palette(name='Random Cycle', palette_id=1)," + " reverse=True, segment_id=1, selected=True," + " speed=16, start=20, stop=30)" + ), }, ], "sync": { From a0a9f3189ee6983653b80df422ba9ac7c81911bd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 8 Jan 2023 15:35:23 -0800 Subject: [PATCH 15/35] Bump gcal_sync to 4.1.1 (#85453) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 100e128c8e313a..9d2d96812a5a6e 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==4.1.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.1.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index eb4c5ee1956000..de73fa309f75e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -751,7 +751,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.0 +gcal-sync==4.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4159aa54f0405c..58687a00dd83e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ gTTS==2.2.4 gassist-text==0.0.7 # homeassistant.components.google -gcal-sync==4.1.0 +gcal-sync==4.1.1 # homeassistant.components.geocaching geocachingapi==0.2.1 From a93ce389e2484e9f657543d794a1bb47de0a24ea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:36:54 +0100 Subject: [PATCH 16/35] Code styling tweaks to the LaMetric integration (#85469) --- homeassistant/components/lametric/config_flow.py | 5 ++++- tests/components/lametric/test_notify.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 7496fc51a4ed16..4d4ebc15850966 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -116,7 +116,10 @@ async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: async def async_step_choice_enter_manual_or_fetch_cloud( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the user's choice of entering the manual credentials or fetching the cloud credentials.""" + """Handle the user's choice. + + Either enter the manual credentials or fetch the cloud credentials. + """ return self.async_show_menu( step_id="choice_enter_manual_or_fetch_cloud", menu_options=["pick_implementation", "manual_entry"], diff --git a/tests/components/lametric/test_notify.py b/tests/components/lametric/test_notify.py index 3b581c81e75b79..7d43e7ba9b05bf 100644 --- a/tests/components/lametric/test_notify.py +++ b/tests/components/lametric/test_notify.py @@ -35,7 +35,9 @@ async def test_notification_defaults( NOTIFY_DOMAIN, NOTIFY_SERVICE, { - ATTR_MESSAGE: "Try not to become a man of success. Rather become a man of value", + ATTR_MESSAGE: ( + "Try not to become a man of success. Rather become a man of value" + ), }, blocking=True, ) @@ -118,7 +120,7 @@ async def test_notification_error( NOTIFY_DOMAIN, NOTIFY_SERVICE, { - ATTR_MESSAGE: "It's failure that gives you the proper perspective on success", + ATTR_MESSAGE: "It's failure that gives you the proper perspective", }, blocking=True, ) From 882975bd5825dab31a6e74f4e68e09310b22d160 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 8 Jan 2023 17:39:26 -0600 Subject: [PATCH 17/35] Handle timeouts in Sonos, reduce logging noise (#85461) --- homeassistant/components/sonos/__init__.py | 15 +++++++++------ homeassistant/components/sonos/helpers.py | 3 ++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 1c9ffc0264780a..095267072ac6da 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any, Optional, cast from urllib.parse import urlparse +from requests.exceptions import Timeout from soco import events_asyncio, zonegroupstate import soco.config as soco_config from soco.core import SoCo @@ -223,13 +224,13 @@ def _async_add_visible_zones(subscription_succeeded: bool = False) -> None: async def async_subscription_failed(now: datetime.datetime) -> None: """Fallback logic if the subscription callback never arrives.""" - await sub.unsubscribe() _LOGGER.warning( "Subscription to %s failed, attempting to poll directly", ip_address ) try: + await sub.unsubscribe() await self.hass.async_add_executor_job(soco.zone_group_state.poll, soco) - except (OSError, SoCoException) as ex: + except (OSError, SoCoException, Timeout) as ex: _LOGGER.warning( "Fallback pollling to %s failed, setup cannot continue: %s", ip_address, @@ -322,8 +323,8 @@ def _add_speaker( new_coordinator.setup(soco) coord_dict[soco.household_id] = new_coordinator speaker.setup(self.entry) - except (OSError, SoCoException): - _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) + except (OSError, SoCoException, Timeout) as ex: + _LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex) async def async_poll_manual_hosts( self, now: datetime.datetime | None = None @@ -344,8 +345,10 @@ def get_sync_attributes(soco: SoCo) -> set[SoCo]: get_sync_attributes, soco, ) - except OSError: - _LOGGER.warning("Could not get visible Sonos devices from %s", ip_addr) + except (OSError, SoCoException, Timeout) as ex: + _LOGGER.warning( + "Could not get visible Sonos devices from %s: %s", ip_addr, ex + ) else: if new_hosts := { x.ip_address diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 552d104786eca8..f1805eda054ea6 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -5,6 +5,7 @@ import logging from typing import TYPE_CHECKING, Any, TypeVar, overload +from requests.exceptions import Timeout from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException from typing_extensions import Concatenate, ParamSpec @@ -65,7 +66,7 @@ def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) try: result = funct(self, *args, **kwargs) - except (OSError, SoCoException, SoCoUPnPException) as err: + except (OSError, SoCoException, SoCoUPnPException, Timeout) as err: error_code = getattr(err, "error_code", None) function = funct.__qualname__ if errorcodes and error_code in errorcodes: From 8bcb52270f8571442b47949179e0daf1b46b3756 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:40:08 +0100 Subject: [PATCH 18/35] Code styling tweaks to core entity components (#85460) --- homeassistant/components/calendar/trigger.py | 7 ++++--- homeassistant/components/camera/img_util.py | 5 ++++- homeassistant/components/light/__init__.py | 11 ++++++----- .../components/media_player/__init__.py | 3 ++- .../components/media_player/browse_media.py | 5 +++-- homeassistant/components/number/__init__.py | 8 ++++++-- homeassistant/components/sensor/__init__.py | 16 +++++++++------- homeassistant/components/sensor/recorder.py | 17 ++++++++++++++--- homeassistant/components/stt/__init__.py | 3 ++- homeassistant/components/tts/__init__.py | 15 ++++++++++++--- homeassistant/components/update/__init__.py | 8 ++++---- homeassistant/components/update/recorder.py | 2 +- homeassistant/components/weather/__init__.py | 8 ++++++-- 13 files changed, 73 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 0fdb7259c9db83..1e51c746e183d0 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -84,9 +84,10 @@ def async_detach(self) -> None: async def _fetch_events(self, last_endtime: datetime.datetime) -> None: """Update the set of eligible events.""" - # Use a sliding window for selecting in scope events in the next interval. The event - # search range is offset, then the fire time of the returned events are offset again below. - # Event time ranges are exclusive so the end time is expanded by 1sec + # Use a sliding window for selecting in scope events in the next interval. + # The event search range is offset, then the fire time of the returned events + # are offset again below. Event time ranges are exclusive so the end time + # is expanded by 1sec. start_time = last_endtime - self._offset end_time = start_time + UPDATE_INTERVAL + datetime.timedelta(seconds=1) _LOGGER.debug( diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py index 3aadc5c454cc3f..87bc0e14fbaa0f 100644 --- a/homeassistant/components/camera/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -38,7 +38,10 @@ def find_supported_scaling_factor( def scale_jpeg_camera_image(cam_image: Image, width: int, height: int) -> bytes: - """Scale a camera image as close as possible to one of the supported scaling factors.""" + """Scale a camera image. + + Scale as close as possible to one of the supported scaling factors. + """ turbo_jpeg = TurboJPEGSingleton.instance() if not turbo_jpeg: return cam_image.content diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index acc6252d3b34cc..24368ccc1a39ac 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -434,9 +434,8 @@ async def async_handle_light_on_service( ): profiles.apply_default(light.entity_id, light.is_on, params) - legacy_supported_color_modes = ( - light._light_internal_supported_color_modes # pylint: disable=protected-access - ) + # pylint: disable=protected-access + legacy_supported_color_modes = light._light_internal_supported_color_modes supported_color_modes = light.supported_color_modes # If a color temperature is specified, emulate it if not supported by the light @@ -504,8 +503,10 @@ async def async_handle_light_on_service( params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: # https://github.com/python/mypy/issues/13673 - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] - *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, # type: ignore[call-arg] + light.min_color_temp_kelvin, + light.max_color_temp_kelvin, ) elif ColorMode.HS in supported_color_modes: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index fa2c5465443957..341c9468d1188c 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1201,7 +1201,8 @@ async def websocket_browse_media( """ Browse media available to the media_player entity. - To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() + To use, media_player integrations can implement + MediaPlayerEntity.async_browse_media() """ component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] player = component.get_entity(msg["entity_id"]) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 81ded203e75e16..d1328a851d250d 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -51,8 +51,9 @@ def async_process_play_media_url( "Not signing path for content with query param" ) elif parsed.path.startswith(PATHS_WITHOUT_AUTH): - # We don't sign this path if it doesn't need auth. Although signing itself can't hurt, - # some devices are unable to handle long URLs and the auth signature might push it over. + # We don't sign this path if it doesn't need auth. Although signing itself can't + # hurt, some devices are unable to handle long URLs and the auth signature might + # push it over. pass else: signed_path = async_sign_path( diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 2c4794619da4a4..e4ed8bb1d3e9bc 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -424,7 +424,9 @@ def __post_init__(self) -> None: or self.step is not None or self.unit_of_measurement is not None ): - if self.__class__.__name__ == "NumberEntityDescription": # type: ignore[unreachable] + if ( # type: ignore[unreachable] + self.__class__.__name__ == "NumberEntityDescription" + ): caller = inspect.stack()[2] module = inspect.getmodule(caller[0]) else: @@ -668,7 +670,9 @@ def unit_of_measurement(self) -> str | None: hasattr(self, "entity_description") and self.entity_description.unit_of_measurement is not None ): - return self.entity_description.unit_of_measurement # type: ignore[unreachable] + return ( # type: ignore[unreachable] + self.entity_description.unit_of_measurement + ) native_unit_of_measurement = self.native_unit_of_measurement diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 067adb128170a2..4a5ff3de893061 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -12,7 +12,9 @@ from typing import Any, Final, cast, final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( # noqa: F401, pylint: disable=[hass-deprecated-import] + +# pylint: disable=[hass-deprecated-import] +from homeassistant.const import ( # noqa: F401 CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, @@ -350,12 +352,12 @@ def suggested_unit_of_measurement(self) -> str | None: For sensors without a `unique_id`, this takes precedence over legacy temperature conversion rules only. - For sensors with a `unique_id`, this is applied only if the unit is not set by the user, - and takes precedence over automatic device-class conversion rules. + For sensors with a `unique_id`, this is applied only if the unit is not set by + the user, and takes precedence over automatic device-class conversion rules. Note: - suggested_unit_of_measurement is stored in the entity registry the first time - the entity is seen, and then never updated. + suggested_unit_of_measurement is stored in the entity registry the first + time the entity is seen, and then never updated. """ if hasattr(self, "_attr_suggested_unit_of_measurement"): return self._attr_suggested_unit_of_measurement @@ -367,8 +369,8 @@ def suggested_unit_of_measurement(self) -> str | None: @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" - # Highest priority, for registered entities: unit set by user, with fallback to unit suggested - # by integration or secondary fallback to unit conversion rules + # Highest priority, for registered entities: unit set by user,with fallback to + # unit suggested by integration or secondary fallback to unit conversion rules if self._sensor_option_unit_of_measurement: return self._sensor_option_unit_of_measurement diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 412ea3d4d5d909..5224514f0e4f52 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -507,9 +507,19 @@ def _compile_statistics( # noqa: C901 # Make calculations stat: StatisticData = {"start": start} if "max" in wanted_statistics[entity_id]: - stat["max"] = max(*itertools.islice(zip(*fstates), 1)) # type: ignore[typeddict-item] + stat["max"] = max( + *itertools.islice( + zip(*fstates), # type: ignore[typeddict-item] + 1, + ) + ) if "min" in wanted_statistics[entity_id]: - stat["min"] = min(*itertools.islice(zip(*fstates), 1)) # type: ignore[typeddict-item] + stat["min"] = min( + *itertools.islice( + zip(*fstates), # type: ignore[typeddict-item] + 1, + ) + ) if "mean" in wanted_statistics[entity_id]: stat["mean"] = _time_weighted_average(fstates, start, end) @@ -519,7 +529,8 @@ def _compile_statistics( # noqa: C901 new_state = old_state = None _sum = 0.0 if entity_id in last_stats: - # We have compiled history for this sensor before, use that as a starting point + # We have compiled history for this sensor before, + # use that as a starting point. last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] if old_last_reset is not None: last_reset = old_last_reset = old_last_reset.isoformat() diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 1d68b0a954b012..94e08d25363553 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -231,7 +231,8 @@ async def get(self, request: web.Request, provider: str) -> web.Response: def metadata_from_header(request: web.Request) -> SpeechMetadata: """Extract STT metadata from header. - X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de + X-Speech-Content: + format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de """ try: data = request.headers[istr("X-Speech-Content")].split(";") diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index c07df07ae4f742..b08487fc84263a 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -621,9 +621,18 @@ def write_tags( if not tts_file.tags: tts_file.add_tags() if isinstance(tts_file.tags, ID3): - tts_file["artist"] = ID3Text(encoding=3, text=artist) # type: ignore[no-untyped-call] - tts_file["album"] = ID3Text(encoding=3, text=album) # type: ignore[no-untyped-call] - tts_file["title"] = ID3Text(encoding=3, text=message) # type: ignore[no-untyped-call] + tts_file["artist"] = ID3Text( + encoding=3, + text=artist, # type: ignore[no-untyped-call] + ) + tts_file["album"] = ID3Text( + encoding=3, + text=album, # type: ignore[no-untyped-call] + ) + tts_file["title"] = ID3Text( + encoding=3, + text=message, # type: ignore[no-untyped-call] + ) else: tts_file["artist"] = artist tts_file["album"] = album diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index f8d00300d811fd..092076657480f1 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -326,16 +326,16 @@ def install(self, version: str | None, backup: bool, **kwargs: Any) -> None: async def async_release_notes(self) -> str | None: """Return full release notes. - This is suitable for a long changelog that does not fit in the release_summary property. - The returned string can contain markdown. + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. """ return await self.hass.async_add_executor_job(self.release_notes) def release_notes(self) -> str | None: """Return full release notes. - This is suitable for a long changelog that does not fit in the release_summary property. - The returned string can contain markdown. + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. """ raise NotImplementedError() diff --git a/homeassistant/components/update/recorder.py b/homeassistant/components/update/recorder.py index 1b22360761ff01..408937c4f3159e 100644 --- a/homeassistant/components/update/recorder.py +++ b/homeassistant/components/update/recorder.py @@ -9,5 +9,5 @@ @callback def exclude_attributes(hass: HomeAssistant) -> set[str]: - """Exclude large and chatty update attributes from being recorded in the database.""" + """Exclude large and chatty update attributes from being recorded.""" return {ATTR_ENTITY_PICTURE, ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY} diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index fcfba179cd7165..52642c4f1bf98d 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -157,7 +157,8 @@ def round_temperature(temperature: float | None, precision: float) -> float | No class Forecast(TypedDict, total=False): """Typed weather forecast dict. - All attributes are in native units and old attributes kept for backwards compatibility. + All attributes are in native units and old attributes kept + for backwards compatibility. """ condition: str | None @@ -622,7 +623,10 @@ def precision(self) -> float: @final @property def state_attributes(self) -> dict[str, Any]: - """Return the state attributes, converted from native units to user-configured units.""" + """Return the state attributes, converted. + + Attributes are configured from native units to user-configured units. + """ data: dict[str, Any] = {} precision = self.precision From 50d60c5cfcfedd7eb0964ea7df1c0e86647e1ccc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 00:44:09 +0100 Subject: [PATCH 19/35] Code styling tweaks to core helpers (#85441) --- homeassistant/helpers/aiohttp_client.py | 9 ++- homeassistant/helpers/condition.py | 4 +- homeassistant/helpers/config_validation.py | 23 +++--- homeassistant/helpers/deprecation.py | 2 +- homeassistant/helpers/device_registry.py | 24 ++++-- homeassistant/helpers/entity.py | 24 ++++-- homeassistant/helpers/entity_component.py | 8 +- homeassistant/helpers/entity_platform.py | 22 ++++-- homeassistant/helpers/entity_registry.py | 6 +- homeassistant/helpers/event.py | 4 +- homeassistant/helpers/location.py | 19 +++-- homeassistant/helpers/restore_state.py | 4 +- .../helpers/schema_config_entry_flow.py | 18 ++--- homeassistant/helpers/sensor.py | 2 +- homeassistant/helpers/service.py | 15 +++- homeassistant/helpers/state.py | 6 +- homeassistant/helpers/storage.py | 5 +- homeassistant/helpers/template.py | 76 +++++++++++++------ homeassistant/helpers/template_entity.py | 3 +- homeassistant/helpers/translation.py | 4 +- homeassistant/helpers/trigger.py | 10 ++- homeassistant/helpers/update_coordinator.py | 4 +- 22 files changed, 192 insertions(+), 100 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2558b5d0896234..ca615bb323bc2a 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -124,9 +124,14 @@ def _async_create_clientsession( # 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[assignment] + clientsession._default_headers = MappingProxyType( # type: ignore[assignment] + {USER_AGENT: SERVER_SOFTWARE}, + ) - clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore[assignment] + clientsession.close = warn_use( # type: ignore[assignment] + clientsession.close, + WARN_CLOSE_MSG, + ) if auto_cleanup_method: auto_cleanup_method(hass, clientsession) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index feb7a2a17ae51b..d07ddcb42a98b1 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -611,8 +611,8 @@ def sun( # Special case: before sunrise OR after sunset # This will handle the very rare case in the polar region when the sun rises/sets # but does not set/rise. - # However this entire condition does not handle those full days of darkness or light, - # the following should be used instead: + # However this entire condition does not handle those full days of darkness + # or light, the following should be used instead: # # condition: # condition: state diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ce2d0740d6671c..e7fcf6723188c7 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -778,12 +778,12 @@ def _deprecated_or_removed( raise_if_present: bool, option_removed: bool, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and provide a replacement (if exists) or fail. + """Log key as deprecated and provide a replacement (if exists) or fail. Expected behavior: - Outputs or throws the appropriate deprecation warning if key is detected - - Outputs or throws the appropriate error if key is detected and removed from support + - Outputs or throws the appropriate error if key is detected + and removed from support - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided @@ -809,7 +809,10 @@ def validator(config: dict) -> dict: """Check if key is in config and log warning or error.""" if key in config: try: - near = f"near {config.__config_file__}:{config.__line__} " # type: ignore[attr-defined] + near = ( + f"near {config.__config_file__}" # type: ignore[attr-defined] + f":{config.__line__} " + ) except AttributeError: near = "" arguments: tuple[str, ...] @@ -851,11 +854,11 @@ def deprecated( default: Any | None = None, raise_if_present: bool | None = False, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and provide a replacement (if exists). + """Log key as deprecated and provide a replacement (if exists). Expected behavior: - - Outputs the appropriate deprecation warning if key is detected or raises an exception + - Outputs the appropriate deprecation warning if key is detected + or raises an exception - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided @@ -876,11 +879,11 @@ def removed( default: Any | None = None, raise_if_present: bool | None = True, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and fail the config validation. + """Log key as deprecated and fail the config validation. Expected behavior: - - Outputs the appropriate error if key is detected and removed from support or raises an exception + - Outputs the appropriate error if key is detected and removed from + support or raises an exception. """ return _deprecated_or_removed( key, diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index a132536b53f105..c737d75dae04ca 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -115,7 +115,7 @@ def deprecated_cls(*args: _P.args, **kwargs: _P.kwargs) -> _R: def deprecated_function( replacement: str, ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: - """Mark function as deprecated and provide a replacement function to be used instead.""" + """Mark function as deprecated and provide a replacement to be used instead.""" def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate function as deprecated.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a7c1ebdb434602..6f2dc22f1ddb00 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -161,7 +161,9 @@ async def _async_migrate_func( device.setdefault("configuration_url", None) device.setdefault("disabled_by", None) try: - device["entry_type"] = DeviceEntryType(device.get("entry_type")) # type: ignore[arg-type] + device["entry_type"] = DeviceEntryType( + device.get("entry_type"), # type: ignore[arg-type] + ) except ValueError: device["entry_type"] = None device.setdefault("name_by_user", None) @@ -550,7 +552,10 @@ async def async_load(self) -> None: config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 - connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + connections={ + tuple(conn) # type: ignore[misc] + for conn in device["connections"] + }, disabled_by=DeviceEntryDisabler(device["disabled_by"]) if device["disabled_by"] else None, @@ -559,7 +564,10 @@ async def async_load(self) -> None: else None, hw_version=device["hw_version"], id=device["id"], - identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] + identifiers={ + tuple(iden) # type: ignore[misc] + for iden in device["identifiers"] + }, manufacturer=device["manufacturer"], model=device["model"], name_by_user=device["name_by_user"], @@ -572,8 +580,14 @@ async def async_load(self) -> None: deleted_devices[device["id"]] = DeletedDeviceEntry( config_entries=set(device["config_entries"]), # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 - connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] - identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] + connections={ + tuple(conn) # type: ignore[misc] + for conn in device["connections"] + }, + identifiers={ + tuple(iden) # type: ignore[misc] + for iden in device["identifiers"] + }, id=device["id"], orphaned_timestamp=device["orphaned_timestamp"], ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 745f9b0ba5369b..f9bb1effeb25f3 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -185,10 +185,11 @@ class EntityCategory(StrEnum): - Not be included in indirect service calls to devices or areas """ - # Config: An entity which allows changing the configuration of a device + # Config: An entity which allows changing the configuration of a device. CONFIG = "config" - # Diagnostic: An entity exposing some configuration parameter or diagnostics of a device + # Diagnostic: An entity exposing some configuration parameter, + # or diagnostics of a device. DIAGNOSTIC = "diagnostic" @@ -198,13 +199,16 @@ class EntityCategory(StrEnum): class EntityPlatformState(Enum): """The platform state of an entity.""" - # Not Added: Not yet added to a platform, polling updates are written to the state machine + # Not Added: Not yet added to a platform, polling updates + # are written to the state machine. NOT_ADDED = auto() - # Added: Added to a platform, polling updates are written to the state machine + # Added: Added to a platform, polling updates + # are written to the state machine. ADDED = auto() - # Removed: Removed from a platform, polling updates are not written to the state machine + # Removed: Removed from a platform, polling updates + # are not written to the state machine. REMOVED = auto() @@ -458,7 +462,10 @@ def context_recent_time(self) -> timedelta: @property def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ if hasattr(self, "_attr_entity_registry_enabled_default"): return self._attr_entity_registry_enabled_default if hasattr(self, "entity_description"): @@ -467,7 +474,10 @@ def entity_registry_enabled_default(self) -> bool: @property def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added to the entity registry.""" + """Return if the entity should be visible when first added. + + This only applies when fist added to the entity registry. + """ if hasattr(self, "_attr_entity_registry_visible_default"): return self._attr_entity_registry_visible_default if hasattr(self, "entity_description"): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 1932c0397a1854..f9cd7d979c8be0 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -90,11 +90,11 @@ def __init__( @property def entities(self) -> Iterable[_EntityT]: - """ - Return an iterable that returns all entities. + """Return an iterable that returns all entities. - As the underlying dicts may change when async context is lost, callers that - iterate over this asynchronously should make a copy using list() before iterating. + As the underlying dicts may change when async context is lost, + callers that iterate over this asynchronously should make a copy + using list() before iterating. """ return chain.from_iterable( platform.entities.values() # type: ignore[misc] diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 622fa4c1751daf..18dd9aea3448a0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -158,12 +158,16 @@ def _get_parallel_updates_semaphore( ) -> asyncio.Semaphore | None: """Get or create a semaphore for parallel updates. - Semaphore will be created on demand because we base it off if update method is async or not. + Semaphore will be created on demand because we base it off if update + method is async or not. - If parallel updates is set to 0, we skip the semaphore. - If parallel updates is set to a number, we initialize the semaphore to that number. - The default value for parallel requests is decided based on the first entity that is added to Home Assistant. - It's 0 if the entity defines the async_update method, else it's 1. + - If parallel updates is set to 0, we skip the semaphore. + - If parallel updates is set to a number, we initialize the semaphore + to that number. + + The default value for parallel requests is decided based on the first + entity that is added to Home Assistant. It's 0 if the entity defines + the async_update method, else it's 1. """ if self.parallel_updates_created: return self.parallel_updates @@ -566,7 +570,9 @@ async def _async_add_entity( # noqa: C901 "via_device", ): if key in device_info: - processed_dev_info[key] = device_info[key] # type: ignore[literal-required] + processed_dev_info[key] = device_info[ + key # type: ignore[literal-required] + ] if "configuration_url" in device_info: if device_info["configuration_url"] is None: @@ -586,7 +592,9 @@ async def _async_add_entity( # noqa: C901 ) try: - device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] + device = device_registry.async_get_or_create( + **processed_dev_info # type: ignore[arg-type] + ) device_id = device.id except RequiredParameterMissing: pass diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index da4a8aae6e115f..54ed93aebb98ba 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -119,7 +119,8 @@ class RegistryEntry: has_entity_name: bool = attr.ib(default=False) name: str | None = attr.ib(default=None) options: EntityOptionsType = attr.ib( - default=None, converter=attr.converters.default_if_none(factory=dict) # type: ignore[misc] + default=None, + converter=attr.converters.default_if_none(factory=dict), # type: ignore[misc] ) # As set by integration original_device_class: str | None = attr.ib(default=None) @@ -780,8 +781,7 @@ def async_update_entity_platform( new_unique_id: str | UndefinedType = UNDEFINED, new_device_id: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: - """ - Update entity platform. + """Update entity platform. This should only be used when an entity needs to be migrated between integrations. diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 00933ba77c6d6c..22c613656e39fe 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -710,8 +710,8 @@ def async_track_state_change_filtered( Returns ------- - Object used to update the listeners (async_update_listeners) with a new TrackStates or - cancel the tracking (async_remove). + Object used to update the listeners (async_update_listeners) with a new + TrackStates or cancel the tracking (async_remove). """ tracker = _TrackStateChangeFiltered(hass, track_states, action) diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index a22d5fddf0c05c..086150115dae25 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -50,8 +50,11 @@ def find_coordinates( ) -> str | None: """Try to resolve the a location from a supplied name or entity_id. - Will recursively resolve an entity if pointed to by the state of the supplied entity. - Returns coordinates in the form of '90.000,180.000', an address or the state of the last resolved entity. + Will recursively resolve an entity if pointed to by the state of the supplied + entity. + + Returns coordinates in the form of '90.000,180.000', an address or + the state of the last resolved entity. """ # Check if a friendly name of a zone was supplied if (zone_coords := resolve_zone(hass, name)) is not None: @@ -70,7 +73,9 @@ def find_coordinates( zone_entity = hass.states.get(f"zone.{entity_state.state}") if has_location(zone_entity): # type: ignore[arg-type] _LOGGER.debug( - "%s is in %s, getting zone location", name, zone_entity.entity_id # type: ignore[union-attr] + "%s is in %s, getting zone location", + name, + zone_entity.entity_id, # type: ignore[union-attr] ) return _get_location_from_attributes(zone_entity) # type: ignore[arg-type] @@ -97,12 +102,16 @@ def find_coordinates( _LOGGER.debug("Resolving nested entity_id: %s", entity_state.state) return find_coordinates(hass, entity_state.state, recursion_history) - # Might be an address, coordinates or anything else. This has to be checked by the caller. + # Might be an address, coordinates or anything else. + # This has to be checked by the caller. return entity_state.state def resolve_zone(hass: HomeAssistant, zone_name: str) -> str | None: - """Get a lat/long from a zones friendly_name or None if no zone is found by that friendly_name.""" + """Get a lat/long from a zones friendly_name. + + None is returned if no zone is found by that friendly_name. + """ states = hass.states.async_all("zone") for state in states: if state.name == zone_name: diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index c31fe0f3ce4d3e..073d1879a82d4e 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -303,7 +303,9 @@ async def _async_get_restored_data(self) -> StoredState | None: """Get data stored for an entity, if any.""" if self.hass is None or self.entity_id is None: # Return None if this entity isn't added to hass yet - _LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable] + _LOGGER.warning( # type: ignore[unreachable] + "Cannot get last state. Entity not added to hass" + ) return None data = await RestoreStateData.async_get_instance(self.hass) if self.entity_id not in data.last_states: diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 4e319b20cb6bfa..c4a274aa6bcd3d 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -60,9 +60,9 @@ class SchemaFlowFormStep(SchemaFlowStep): """Optional property to identify next step. - If `next_step` is a function, it is called if the schema validates successfully or - if no schema is defined. The `next_step` function is passed the union of config entry - options and user input from previous steps. If the function returns None, the flow is - ended with `FlowResultType.CREATE_ENTRY`. + if no schema is defined. The `next_step` function is passed the union of + config entry options and user input from previous steps. If the function returns + None, the flow is ended with `FlowResultType.CREATE_ENTRY`. - If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`. """ @@ -71,11 +71,11 @@ class SchemaFlowFormStep(SchemaFlowStep): ] | None | UndefinedType = UNDEFINED """Optional property to populate suggested values. - - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested value - from an option with the same key. + - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested + value from an option with the same key. - Note: if a step is retried due to a validation failure, then the user input will have - priority over the suggested values. + Note: if a step is retried due to a validation failure, then the user input will + have priority over the suggested values. """ @@ -331,8 +331,8 @@ def async_options_flow_finished( ) -> None: """Take necessary actions after the options flow is finished, if needed. - The options parameter contains config entry options, which is the union of stored - options and user input from the options flow steps. + The options parameter contains config entry options, which is the union of + stored options and user input from the options flow steps. """ @callback diff --git a/homeassistant/helpers/sensor.py b/homeassistant/helpers/sensor.py index f206ac55bddf54..96e6b83a16713a 100644 --- a/homeassistant/helpers/sensor.py +++ b/homeassistant/helpers/sensor.py @@ -17,7 +17,7 @@ def sensor_device_info_to_hass_device_info( sensor_device_info: SensorDeviceInfo, ) -> DeviceInfo: - """Convert a sensor_state_data sensor device info to a Home Assistant device info.""" + """Convert a sensor_state_data sensor device info to a HA device info.""" device_info = DeviceInfo() if sensor_device_info.name is not None: device_info[const.ATTR_NAME] = sensor_device_info.name diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 935f1840db5018..368b8dc253fe20 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -371,7 +371,8 @@ def async_extract_referenced_entity_ids( return selected for ent_entry in ent_reg.entities.values(): - # Do not add entities which are hidden or which are config or diagnostic entities + # Do not add entities which are hidden or which are config + # or diagnostic entities. if ent_entry.entity_category is not None or ent_entry.hidden_by is not None: continue @@ -489,7 +490,10 @@ async def async_get_all_descriptions( # Cache missing descriptions if description is None: domain_yaml = loaded[domain] - yaml_description = domain_yaml.get(service, {}) # type: ignore[union-attr] + + yaml_description = domain_yaml.get( # type: ignore[union-attr] + service, {} + ) # Don't warn for missing services, because it triggers false # positives for things like scripts, that register as a service @@ -706,11 +710,14 @@ async def _handle_entity_call( entity.async_set_context(context) if isinstance(func, str): - result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore[arg-type] + result = hass.async_run_job( + partial(getattr(entity, func), **data) # type: ignore[arg-type] + ) else: result = hass.async_run_job(func, entity, data) - # Guard because callback functions do not return a task when passed to async_run_job. + # Guard because callback functions do not return a task when passed to + # async_run_job. if result is not None: await result diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 75ca96ea2468f4..21d060f4ba77a3 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -31,8 +31,7 @@ class AsyncTrackStates: - """ - Record the time when the with-block is entered. + """Record the time when the with-block is entered. Add all states that have changed since the start time to the return list when with-block is exited. @@ -119,8 +118,7 @@ async def worker(domain: str, states_by_domain: list[State]) -> None: def state_as_number(state: State) -> float: - """ - Try to coerce our state to a number. + """Try to coerce our state to a number. Raises ValueError if this is not possible. """ diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index bb7b5c850c5f34..73c0d6e1d5547f 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -104,8 +104,9 @@ def path(self): async def async_load(self) -> _T | None: """Load data. - If the expected version and minor version do not match the given versions, the - migrate function will be invoked with migrate_func(version, minor_version, config). + If the expected version and minor version do not match the given + versions, the migrate function will be invoked with + migrate_func(version, minor_version, config). Will ensure that when a call comes in while another one is in progress, the second call will wait and return the result of the first call. diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 0c888784629840..78ed392f5fb98a 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -255,20 +255,39 @@ def __init__(self, template: Template) -> None: def __repr__(self) -> str: """Representation of RenderInfo.""" - return f" has_time={self.has_time}" + return ( + f"" + ) def _filter_domains_and_entities(self, entity_id: str) -> bool: - """Template should re-render if the entity state changes when we match specific domains or entities.""" + """Template should re-render if the entity state changes. + + Only when we match specific domains or entities. + """ return ( split_entity_id(entity_id)[0] in self.domains or entity_id in self.entities ) def _filter_entities(self, entity_id: str) -> bool: - """Template should re-render if the entity state changes when we match specific entities.""" + """Template should re-render if the entity state changes. + + Only when we match specific entities. + """ return entity_id in self.entities def _filter_lifecycle_domains(self, entity_id: str) -> bool: - """Template should re-render if the entity is added or removed with domains watched.""" + """Template should re-render if the entity is added or removed. + + Only with domains watched. + """ return split_entity_id(entity_id)[0] in self.domains_lifecycle def result(self) -> str: @@ -359,7 +378,11 @@ def _env(self) -> TemplateEnvironment: wanted_env = _ENVIRONMENT ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: - ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited, self._strict) # type: ignore[no-untyped-call] + ret = self.hass.data[wanted_env] = TemplateEnvironment( # type: ignore[no-untyped-call] + self.hass, + self._limited, + self._strict, + ) return ret def ensure_valid(self) -> None: @@ -382,7 +405,8 @@ def render( ) -> Any: """Render given template. - If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. + If limited is True, the template is not allowed to access any function + or filter depending on hass or the state machine. """ if self.is_static: if not parse_result or self.hass and self.hass.config.legacy_templates: @@ -407,7 +431,8 @@ def async_render( This method must be run in the event loop. - If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. + If limited is True, the template is not allowed to access any function + or filter depending on hass or the state machine. """ if self.is_static: if not parse_result or self.hass and self.hass.config.legacy_templates: @@ -1039,11 +1064,11 @@ def device_entities(hass: HomeAssistant, _device_id: str) -> Iterable[str]: def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: - """ - Get entity ids for entities tied to an integration/domain. + """Get entity ids for entities tied to an integration/domain. Provide entry_name as domain to get all entity id's for a integration/domain - or provide a config entry title for filtering between instances of the same integration. + or provide a config entry title for filtering between instances of the same + integration. """ # first try if this is a config entry match conf_entry = next( @@ -1643,8 +1668,7 @@ def fail_when_undefined(value): def min_max_from_filter(builtin_filter: Any, name: str) -> Any: - """ - Convert a built-in min/max Jinja filter to a global function. + """Convert a built-in min/max Jinja filter to a global function. The parameters may be passed as an iterable or as separate arguments. """ @@ -1667,16 +1691,17 @@ def wrapper(environment: jinja2.Environment, *args: Any, **kwargs: Any) -> Any: def average(*args: Any, default: Any = _SENTINEL) -> Any: - """ - Filter and function to calculate the arithmetic mean of an iterable or of two or more arguments. + """Filter and function to calculate the arithmetic mean. + + Calculates of an iterable or of two or more arguments. The parameters may be passed as an iterable or as separate arguments. """ if len(args) == 0: raise TypeError("average expected at least 1 argument, got 0") - # If first argument is iterable and more than 1 argument provided but not a named default, - # then use 2nd argument as default. + # If first argument is iterable and more than 1 argument provided but not a named + # default, then use 2nd argument as default. if isinstance(args[0], Iterable): average_list = args[0] if len(args) > 1 and default is _SENTINEL: @@ -1884,8 +1909,7 @@ def today_at(time_str: str = "") -> datetime: def relative_time(value): - """ - Take a datetime and return its "age" as a string. + """Take a datetime and return its "age" as a string. The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will @@ -1953,7 +1977,7 @@ def _render_with_context( class LoggingUndefined(jinja2.Undefined): """Log on undefined variables.""" - def _log_message(self): + def _log_message(self) -> None: template, action = template_cv.get() or ("", "rendering or compiling") _LOGGER.warning( "Template variable warning: %s when %s '%s'", @@ -1975,7 +1999,7 @@ def _fail_with_undefined_error(self, *args, **kwargs): ) raise ex - def __str__(self): + def __str__(self) -> str: """Log undefined __str___.""" self._log_message() return super().__str__() @@ -1985,7 +2009,7 @@ def __iter__(self): self._log_message() return super().__iter__() - def __bool__(self): + def __bool__(self) -> bool: """Log undefined __bool___.""" self._log_message() return super().__bool__() @@ -1996,13 +2020,16 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def __init__(self, hass, limited=False, strict=False): """Initialise template environment.""" + undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] if not strict: undefined = LoggingUndefined else: undefined = jinja2.StrictUndefined super().__init__(undefined=undefined) self.hass = hass - self.template_cache = weakref.WeakValueDictionary() + self.template_cache: weakref.WeakValueDictionary[ + str | jinja2.nodes.Template, CodeType | str | None + ] = weakref.WeakValueDictionary() self.filters["round"] = forgiving_round self.filters["multiply"] = multiply self.filters["log"] = logarithm @@ -2138,8 +2165,8 @@ def wrapper(_: Any, *args: _P.args, **kwargs: _P.kwargs) -> _R: if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. - def unsupported(name): - def warn_unsupported(*args, **kwargs): + def unsupported(name: str) -> Callable[[], NoReturn]: + def warn_unsupported(*args: Any, **kwargs: Any) -> NoReturn: raise TemplateError( f"Use of '{name}' is not supported in limited templates" ) @@ -2247,7 +2274,6 @@ def compile( defer_init, ) - cached: CodeType | str | None if (cached := self.template_cache.get(source)) is None: cached = self.template_cache[source] = super().compile(source) diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index e824b2f2c8b7b2..e3907217988b3a 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -268,8 +268,7 @@ def add_template_attribute( on_update: Callable[[Any], None] | None = None, none_on_template_error: bool = False, ) -> None: - """ - Call in the constructor to add a template linked to a attribute. + """Call in the constructor to add a template linked to a attribute. Parameters ---------- diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index fc41ba1d6f329c..0c59035fc76c01 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -257,7 +257,9 @@ def _build_category_cache( _merge_resources if category == "state" else _build_resources ) new_resources: Mapping[str, dict[str, Any] | str] - new_resources = resource_func(translation_strings, components, category) # type: ignore[assignment] + new_resources = resource_func( # type: ignore[assignment] + translation_strings, components, category + ) for component, resource in new_resources.items(): category_cache: dict[str, Any] = cached.setdefault( diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 1bf9874988d314..1e364878f03c87 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -127,7 +127,10 @@ def async_attach_trigger( action: TriggerActionType, variables: dict[str, Any], ) -> CALLBACK_TYPE: - """Attach an action to a trigger entry. Existing or future plugs registered will be attached.""" + """Attach an action to a trigger entry. + + Existing or future plugs registered will be attached. + """ reg = PluggableAction.async_get_registry(hass) key = tuple(sorted(trigger.items())) entry = reg[key] @@ -163,7 +166,10 @@ def async_register( @callback def _remove() -> None: - """Remove plug from registration, and clean up entry if there are no actions or plugs registered.""" + """Remove plug from registration. + + Clean up entry if there are no actions or plugs registered. + """ assert self._entry self._entry.plugs.remove(self) if not self._entry.actions and not self._entry.plugs: diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index c1ffc5bddeb673..9f9b1a30ba6e21 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -403,7 +403,9 @@ class CoordinatorEntity(BaseCoordinatorEntity[_DataUpdateCoordinatorT]): def __init__( self, coordinator: _DataUpdateCoordinatorT, context: Any = None ) -> None: - """Create the entity with a DataUpdateCoordinator. Passthrough to BaseCoordinatorEntity. + """Create the entity with a DataUpdateCoordinator. + + Passthrough to BaseCoordinatorEntity. Necessary to bind TypeVar to correct scope. """ From 03566cd5118d115b2461ec5dd0469264410788cc Mon Sep 17 00:00:00 2001 From: David Buezas Date: Mon, 9 Jan 2023 01:06:32 +0100 Subject: [PATCH 20/35] Expose async_scanner_devices_by_address from the bluetooth api (#83733) Co-authored-by: J. Nick Koston fixes undefined --- .../components/bluetooth/__init__.py | 5 +- homeassistant/components/bluetooth/api.py | 10 +- .../components/bluetooth/base_scanner.py | 10 ++ homeassistant/components/bluetooth/manager.py | 28 ++-- .../components/bluetooth/wrappers.py | 46 +++---- tests/components/bluetooth/test_api.py | 125 +++++++++++++++++- 6 files changed, 178 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index d0a69bfe37951f..add7dad1a1fb6d 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -58,9 +58,10 @@ async_register_scanner, async_scanner_by_source, async_scanner_count, + async_scanner_devices_by_address, async_track_unavailable, ) -from .base_scanner import BaseHaRemoteScanner, BaseHaScanner +from .base_scanner import BaseHaRemoteScanner, BaseHaScanner, BluetoothScannerDevice from .const import ( BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, @@ -99,6 +100,7 @@ "async_track_unavailable", "async_scanner_by_source", "async_scanner_count", + "async_scanner_devices_by_address", "BaseHaScanner", "BaseHaRemoteScanner", "BluetoothCallbackMatcher", @@ -107,6 +109,7 @@ "BluetoothServiceInfoBleak", "BluetoothScanningMode", "BluetoothCallback", + "BluetoothScannerDevice", "HaBluetoothConnector", "SOURCE_LOCAL", "FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS", diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index cd6b4ac959b85d..6c232e2a42cfca 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -13,7 +13,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import DATA_MANAGER from .manager import BluetoothManager from .match import BluetoothCallbackMatcher @@ -93,6 +93,14 @@ def async_ble_device_from_address( return _get_manager(hass).async_ble_device_from_address(address, connectable) +@hass_callback +def async_scanner_devices_by_address( + hass: HomeAssistant, address: str, connectable: bool = True +) -> list[BluetoothScannerDevice]: + """Return all discovered BluetoothScannerDevice for an address.""" + return _get_manager(hass).async_scanner_devices_by_address(address, connectable) + + @hass_callback def async_address_present( hass: HomeAssistant, address: str, connectable: bool = True diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index 8868b0a0883487..d9fcc750ed410d 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Generator from contextlib import contextmanager +from dataclasses import dataclass import datetime from datetime import timedelta import logging @@ -39,6 +40,15 @@ _LOGGER = logging.getLogger(__name__) +@dataclass +class BluetoothScannerDevice: + """Data for a bluetooth device from a given scanner.""" + + scanner: BaseHaScanner + ble_device: BLEDevice + advertisement: AdvertisementData + + class BaseHaScanner(ABC): """Base class for Ha Scanners.""" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index c863299d206426..91d658cdf5872d 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -29,7 +29,7 @@ from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, @@ -217,18 +217,22 @@ def async_stop(self, event: Event) -> None: uninstall_multiple_bleak_catcher() @hass_callback - def async_get_scanner_discovered_devices_and_advertisement_data_by_address( + def async_scanner_devices_by_address( self, address: str, connectable: bool - ) -> list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]]: - """Get scanner, devices, and advertisement_data by address.""" - types_ = (True,) if connectable else (True, False) - results: list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]] = [] - for type_ in types_: - for scanner in self._get_scanners_by_type(type_): - devices_and_adv_data = scanner.discovered_devices_and_advertisement_data - if device_adv_data := devices_and_adv_data.get(address): - results.append((scanner, *device_adv_data)) - return results + ) -> list[BluetoothScannerDevice]: + """Get BluetoothScannerDevice by address.""" + scanners = self._get_scanners_by_type(True) + if not connectable: + scanners.extend(self._get_scanners_by_type(False)) + return [ + BluetoothScannerDevice(scanner, *device_adv) + for scanner in scanners + if ( + device_adv := scanner.discovered_devices_and_advertisement_data.get( + address + ) + ) + ] @hass_callback def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]: diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index 4a1be63903fc2a..6b463423c73dd2 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -12,11 +12,7 @@ from bleak import BleakClient, BleakError from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type from bleak.backends.device import BLEDevice -from bleak.backends.scanner import ( - AdvertisementData, - AdvertisementDataCallback, - BaseBleakScanner, -) +from bleak.backends.scanner import AdvertisementDataCallback, BaseBleakScanner from bleak_retry_connector import ( NO_RSSI_VALUE, ble_device_description, @@ -28,7 +24,7 @@ from homeassistant.helpers.frame import report from . import models -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice FILTER_UUIDS: Final = "UUIDs" _LOGGER = logging.getLogger(__name__) @@ -149,9 +145,7 @@ def __del__(self) -> None: def _rssi_sorter_with_connection_failure_penalty( - scanner_device_advertisement_data: tuple[ - BaseHaScanner, BLEDevice, AdvertisementData - ], + device: BluetoothScannerDevice, connection_failure_count: dict[BaseHaScanner, int], rssi_diff: int, ) -> float: @@ -168,9 +162,8 @@ def _rssi_sorter_with_connection_failure_penalty( best adapter twice before moving on to the next best adapter since the first failure may be a transient service resolution issue. """ - scanner, _, advertisement_data = scanner_device_advertisement_data - base_rssi = advertisement_data.rssi or NO_RSSI_VALUE - if connect_failures := connection_failure_count.get(scanner): + base_rssi = device.advertisement.rssi or NO_RSSI_VALUE + if connect_failures := connection_failure_count.get(device.scanner): if connect_failures > 1 and not rssi_diff: rssi_diff = 1 return base_rssi - (rssi_diff * connect_failures * 0.51) @@ -300,14 +293,10 @@ def _async_get_best_available_backend_and_device( that has a free connection slot. """ address = self.__address - scanner_device_advertisement_datas = manager.async_get_scanner_discovered_devices_and_advertisement_data_by_address( # noqa: E501 - address, True - ) - sorted_scanner_device_advertisement_datas = sorted( - scanner_device_advertisement_datas, - key=lambda scanner_device_advertisement_data: ( - scanner_device_advertisement_data[2].rssi or NO_RSSI_VALUE - ), + devices = manager.async_scanner_devices_by_address(self.__address, True) + sorted_devices = sorted( + devices, + key=lambda device: device.advertisement.rssi or NO_RSSI_VALUE, reverse=True, ) @@ -315,31 +304,28 @@ def _async_get_best_available_backend_and_device( # to prefer the adapter/scanner with the less failures so # we don't keep trying to connect with an adapter # that is failing - if ( - self.__connect_failures - and len(sorted_scanner_device_advertisement_datas) > 1 - ): + if self.__connect_failures and len(sorted_devices) > 1: # We use the rssi diff between to the top two # to adjust the rssi sorter so that each failure # will reduce the rssi sorter by the diff amount rssi_diff = ( - sorted_scanner_device_advertisement_datas[0][2].rssi - - sorted_scanner_device_advertisement_datas[1][2].rssi + sorted_devices[0].advertisement.rssi + - sorted_devices[1].advertisement.rssi ) adjusted_rssi_sorter = partial( _rssi_sorter_with_connection_failure_penalty, connection_failure_count=self.__connect_failures, rssi_diff=rssi_diff, ) - sorted_scanner_device_advertisement_datas = sorted( - scanner_device_advertisement_datas, + sorted_devices = sorted( + devices, key=adjusted_rssi_sorter, reverse=True, ) - for (scanner, ble_device, _) in sorted_scanner_device_advertisement_datas: + for device in sorted_devices: if backend := self._async_get_backend_for_ble_device( - manager, scanner, ble_device + manager, device.scanner, device.ble_device ): return backend diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index acb09c22ba73c8..c875710d8e585e 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -1,10 +1,18 @@ """Tests for the Bluetooth integration API.""" +from bleak.backends.scanner import AdvertisementData, BLEDevice + from homeassistant.components import bluetooth -from homeassistant.components.bluetooth import async_scanner_by_source +from homeassistant.components.bluetooth import ( + BaseHaRemoteScanner, + BaseHaScanner, + HaBluetoothConnector, + async_scanner_by_source, + async_scanner_devices_by_address, +) -from . import FakeScanner +from . import FakeScanner, MockBleakClient, _get_manager, generate_advertisement_data async def test_scanner_by_source(hass, enable_bluetooth): @@ -16,3 +24,116 @@ async def test_scanner_by_source(hass, enable_bluetooth): assert async_scanner_by_source(hass, "hci2") is hci2_scanner cancel_hci2() assert async_scanner_by_source(hass, "hci2") is None + + +async def test_async_scanner_devices_by_address_connectable(hass, enable_bluetooth): + """Test getting scanner devices by address with connectable devices.""" + manager = _get_manager() + + class FakeInjectableScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeInjectableScanner( + hass, "esp32", "esp32", new_info_callback, connector, False + ) + unsetup = scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"], + service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + scanner.inject_advertisement(switchbot_device, switchbot_device_adv) + assert async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=True + ) == async_scanner_devices_by_address(hass, "44:44:33:11:23:45", connectable=False) + devices = async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=False + ) + assert len(devices) == 1 + assert devices[0].scanner == scanner + assert devices[0].ble_device.name == switchbot_device.name + assert devices[0].advertisement.local_name == switchbot_device_adv.local_name + unsetup() + cancel() + + +async def test_async_scanner_devices_by_address_non_connectable(hass, enable_bluetooth): + """Test getting scanner devices by address with non-connectable devices.""" + manager = _get_manager() + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"], + service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeStaticScanner(BaseHaScanner): + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return [switchbot_device] + + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices and their advertisement data.""" + return {switchbot_device.address: (switchbot_device, switchbot_device_adv)} + + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeStaticScanner(hass, "esp32", "esp32", connector) + cancel = manager.async_register_scanner(scanner, False) + + assert scanner.discovered_devices_and_advertisement_data == { + switchbot_device.address: (switchbot_device, switchbot_device_adv) + } + + assert ( + async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=True + ) + == [] + ) + devices = async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=False + ) + assert len(devices) == 1 + assert devices[0].scanner == scanner + assert devices[0].ble_device.name == switchbot_device.name + assert devices[0].advertisement.local_name == switchbot_device_adv.local_name + cancel() From 40f134b1df68b32ec8a62fb1c46e7f36b39c1e45 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 9 Jan 2023 00:23:57 +0000 Subject: [PATCH 21/35] [ci skip] Translation update --- .../aussie_broadband/translations/uk.json | 10 +++++ .../components/braviatv/translations/uk.json | 5 +++ .../components/brother/translations/he.json | 6 ++- .../components/demo/translations/uk.json | 13 ++++++ .../google_mail/translations/ca.json | 33 ++++++++++++++ .../google_mail/translations/el.json | 33 ++++++++++++++ .../google_mail/translations/et.json | 33 ++++++++++++++ .../google_mail/translations/ru.json | 33 ++++++++++++++ .../google_mail/translations/sk.json | 33 ++++++++++++++ .../google_mail/translations/uk.json | 24 +++++++++++ .../google_mail/translations/zh-Hant.json | 33 ++++++++++++++ .../homeassistant_yellow/translations/el.json | 3 ++ .../components/knx/translations/uk.json | 43 +++++++++++++++++++ .../ld2410_ble/translations/uk.json | 16 +++++++ .../components/mjpeg/translations/uk.json | 30 +++++++++++++ .../components/moon/translations/uk.json | 11 +++++ .../components/nam/translations/uk.json | 12 ++++++ .../components/pi_hole/translations/uk.json | 8 +++- .../components/plugwise/translations/uk.json | 13 ++++++ .../components/purpleair/translations/uk.json | 17 ++++++++ .../components/rainbird/translations/el.json | 34 +++++++++++++++ .../components/rainbird/translations/et.json | 34 +++++++++++++++ .../components/rainbird/translations/ru.json | 34 +++++++++++++++ .../components/rainbird/translations/sk.json | 34 +++++++++++++++ .../components/rainbird/translations/uk.json | 24 +++++++++++ .../rainbird/translations/zh-Hant.json | 34 +++++++++++++++ .../components/reolink/translations/uk.json | 33 ++++++++++++++ .../ruuvi_gateway/translations/uk.json | 8 ++++ .../components/season/translations/uk.json | 29 +++++++++++++ .../components/sensibo/translations/uk.json | 12 ++++++ .../components/sfr_box/translations/sk.json | 1 + .../components/sfr_box/translations/uk.json | 28 ++++++++++++ .../components/starlink/translations/ca.json | 17 ++++++++ .../components/starlink/translations/el.json | 17 ++++++++ .../components/starlink/translations/et.json | 17 ++++++++ .../components/starlink/translations/ru.json | 17 ++++++++ .../components/starlink/translations/sk.json | 17 ++++++++ .../components/starlink/translations/uk.json | 17 ++++++++ .../starlink/translations/zh-Hant.json | 17 ++++++++ .../components/switchbot/translations/uk.json | 30 +++++++++++++ .../components/vacuum/translations/he.json | 8 ++-- .../zeversolar/translations/uk.json | 8 ++++ .../components/zodiac/translations/uk.json | 11 +++++ 43 files changed, 883 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/aussie_broadband/translations/uk.json create mode 100644 homeassistant/components/google_mail/translations/ca.json create mode 100644 homeassistant/components/google_mail/translations/el.json create mode 100644 homeassistant/components/google_mail/translations/et.json create mode 100644 homeassistant/components/google_mail/translations/ru.json create mode 100644 homeassistant/components/google_mail/translations/sk.json create mode 100644 homeassistant/components/google_mail/translations/uk.json create mode 100644 homeassistant/components/google_mail/translations/zh-Hant.json create mode 100644 homeassistant/components/knx/translations/uk.json create mode 100644 homeassistant/components/ld2410_ble/translations/uk.json create mode 100644 homeassistant/components/mjpeg/translations/uk.json create mode 100644 homeassistant/components/moon/translations/uk.json create mode 100644 homeassistant/components/nam/translations/uk.json create mode 100644 homeassistant/components/purpleair/translations/uk.json create mode 100644 homeassistant/components/rainbird/translations/el.json create mode 100644 homeassistant/components/rainbird/translations/et.json create mode 100644 homeassistant/components/rainbird/translations/ru.json create mode 100644 homeassistant/components/rainbird/translations/sk.json create mode 100644 homeassistant/components/rainbird/translations/uk.json create mode 100644 homeassistant/components/rainbird/translations/zh-Hant.json create mode 100644 homeassistant/components/reolink/translations/uk.json create mode 100644 homeassistant/components/ruuvi_gateway/translations/uk.json create mode 100644 homeassistant/components/season/translations/uk.json create mode 100644 homeassistant/components/sensibo/translations/uk.json create mode 100644 homeassistant/components/sfr_box/translations/uk.json create mode 100644 homeassistant/components/starlink/translations/ca.json create mode 100644 homeassistant/components/starlink/translations/el.json create mode 100644 homeassistant/components/starlink/translations/et.json create mode 100644 homeassistant/components/starlink/translations/ru.json create mode 100644 homeassistant/components/starlink/translations/sk.json create mode 100644 homeassistant/components/starlink/translations/uk.json create mode 100644 homeassistant/components/starlink/translations/zh-Hant.json create mode 100644 homeassistant/components/switchbot/translations/uk.json create mode 100644 homeassistant/components/zeversolar/translations/uk.json create mode 100644 homeassistant/components/zodiac/translations/uk.json diff --git a/homeassistant/components/aussie_broadband/translations/uk.json b/homeassistant/components/aussie_broadband/translations/uk.json new file mode 100644 index 00000000000000..78ae029bc90605 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/uk.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json index 5d9e22e59de1e4..dff6e78e079f98 100644 --- a/homeassistant/components/braviatv/translations/uk.json +++ b/homeassistant/components/braviatv/translations/uk.json @@ -17,6 +17,11 @@ "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0441\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u041c\u0435\u0440\u0435\u0436\u0430 - > \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e - > \u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" }, + "pin": { + "data": { + "pin": "PIN \u043a\u043e\u0434" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/brother/translations/he.json b/homeassistant/components/brother/translations/he.json index af3f5750ddb0f4..88d8630c57ed46 100644 --- a/homeassistant/components/brother/translations/he.json +++ b/homeassistant/components/brother/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", + "unsupported_model": "\u05d3\u05d2\u05dd \u05de\u05d3\u05e4\u05e1\u05ea \u05d6\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da." }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -11,7 +12,8 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "type": "\u05e1\u05d5\u05d2 \u05d4\u05de\u05d3\u05e4\u05e1\u05ea" } } } diff --git a/homeassistant/components/demo/translations/uk.json b/homeassistant/components/demo/translations/uk.json index 5ac1ac74708e18..42350106b841df 100644 --- a/homeassistant/components/demo/translations/uk.json +++ b/homeassistant/components/demo/translations/uk.json @@ -1,4 +1,17 @@ { + "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "swing_mode": { + "state": { + "auto": "\u0410\u0432\u0442\u043e" + } + } + } + } + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/google_mail/translations/ca.json b/homeassistant/components/google_mail/translations/ca.json new file mode 100644 index 00000000000000..1b0dcd90d9d6e2 --- /dev/null +++ b/homeassistant/components/google_mail/translations/ca.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Segueix les [instruccions]({more_info_url}) de [la pantalla de consentiment OAuth]({oauth_consent_url}) perqu\u00e8 Home Assistant tingui acc\u00e9s al teu correu de Google. Tamb\u00e9 has de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al teu compte:\n 1. V\u00e9s a [Credencials]({oauth_creds_url}) i fes clic a **Crear credencials**.\n 2. A la llista desplegable, selecciona **ID de client OAuth**.\n 3. Selecciona **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " + }, + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "oauth_error": "S'han rebut dades token inv\u00e0lides.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "auth": { + "title": "Vinculaci\u00f3 amb compte de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Google Mail ha de tornar a autenticar-se amb el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/el.json b/homeassistant/components/google_mail/translations/el.json new file mode 100644 index 00000000000000..75f37324e5b0b5 --- /dev/null +++ b/homeassistant/components/google_mail/translations/el.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]({more_info_url}) \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd [\u03bf\u03b8\u03cc\u03bd\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b1\u03c4\u03ac\u03b8\u03b5\u03c3\u03b7\u03c2 OAuth]({oauth_consent_url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd Home Assistant \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Google Mail \u03c3\u03b1\u03c2. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2:\n1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 [Credentials]({oauth_creds_url}) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **Create Credentials**.\n1. \u0391\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c0\u03c4\u03c5\u03c3\u03c3\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth**.\n1. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **Web application** \u03b3\u03b9\u03b1 \u03c4\u03bf Application Type (\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2).\n\n" + }, + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "auth": { + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Google" + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Google Mail \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/et.json b/homeassistant/components/google_mail/translations/et.json new file mode 100644 index 00000000000000..d9832b863df951 --- /dev/null +++ b/homeassistant/components/google_mail/translations/et.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "J\u00e4rgi [juhiseid]({more_info_url}) [OAuthi n\u00f5usoleku kuva]({oauth_consent_url}), et anda Home Assistantile juurdep\u00e4\u00e4s Google Mailile. Samuti pead looma oma kontoga lingitud rakenduse identimisteabe:\n1. Mine aadressile [Credentials]({oauth_creds_url}) ja kl\u00f5psa **Create Credentials**.\n1. Vali rippmen\u00fc\u00fcst **OAuth kliendi ID**.\n1. Vali rakenduse t\u00fc\u00fcbiks **Veebirakendus**.\n\n" + }, + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "oauth_error": "Saadi sobimatud loaandmed.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "auth": { + "title": "Google'i konto linkimine" + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Google Maili sidumine peab konto uuesti autentima", + "title": "Taastuvasta sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/ru.json b/homeassistant/components/google_mail/translations/ru.json new file mode 100644 index 00000000000000..1a84654e4c9b1a --- /dev/null +++ b/homeassistant/components/google_mail/translations/ru.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a Google Mail. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web application**." + }, + "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_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.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \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.", + "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", + "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.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "auth": { + "title": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Google" + }, + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "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 Google.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/sk.json b/homeassistant/components/google_mail/translations/sk.json new file mode 100644 index 00000000000000..b9c5e6b8ac19ee --- /dev/null +++ b/homeassistant/components/google_mail/translations/sk.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Pod\u013ea [pokynov]({more_info_url}) pre [obrazovka s\u00fahlasu s protokolom OAuth]({oauth_consent_url}) povo\u013ete Asistentovi dom\u00e1cnosti pr\u00edstup k va\u0161ej po\u0161te Google Mail. Mus\u00edte tie\u017e vytvori\u0165 poverenia aplik\u00e1cie prepojen\u00e9 s va\u0161\u00edm \u00fa\u010dtom:\n1. Prejdite na [Credentials]({oauth_creds_url}) a kliknite na **Create Credentials**.\n1. Z rozba\u013eovacieho zoznamu vyberte **ID klienta OAuth**.\n1. Ako Typ aplik\u00e1cie vyberte **Webov\u00e1 aplik\u00e1cia**. \n\n" + }, + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "auth": { + "title": "Prepoji\u0165 \u00fa\u010det Google" + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "description": "Integr\u00e1cia slu\u017eby Google Mail vy\u017eaduje op\u00e4tovn\u00e9 overenie v\u00e1\u0161ho \u00fa\u010dtu", + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/uk.json b/homeassistant/components/google_mail/translations/uk.json new file mode 100644 index 00000000000000..dd8f263d5a79b1 --- /dev/null +++ b/homeassistant/components/google_mail/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0434\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457.", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0456\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u043e" + }, + "step": { + "auth": { + "title": "\u041f\u043e\u0432\u2019\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Google" + }, + "reauth_confirm": { + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Google Mail \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/zh-Hant.json b/homeassistant/components/google_mail/translations/zh-Hant.json new file mode 100644 index 00000000000000..c3fa36a828fcf3 --- /dev/null +++ b/homeassistant/components/google_mail/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "\u8ddf\u96a8[\u8aaa\u660e]({more_info_url})\u4ee5\u8a2d\u5b9a\u81f3 [OAuth \u540c\u610f\u756b\u9762]({oauth_consent_url})\u3001\u4f9b Home Assistant \u5b58\u53d6\u60a8\u7684 Google \u90f5\u4ef6\u3002\u540c\u6642\u9700\u8981\u65b0\u589e\u9023\u7d50\u81f3\u5e33\u865f\u7684\u61c9\u7528\u7a0b\u5f0f\u6191\u8b49\uff1a\n1. \u700f\u89bd\u81f3 [\u6191\u8b49]({oauth_creds_url}) \u9801\u9762\u4e26\u9ede\u9078 **\u5efa\u7acb\u6191\u8b49**\u3002\n1. \u7531\u4e0b\u62c9\u9078\u55ae\u4e2d\u9078\u64c7 **OAuth \u7528\u6236\u7aef ID**\u3002\n1. \u61c9\u7528\u7a0b\u5f0f\u985e\u578b\u5247\u9078\u64c7 **Web \u61c9\u7528\u7a0b\u5f0f**\u3002\n\n" + }, + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "auth": { + "title": "\u9023\u7d50 Google \u5e33\u865f" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Google \u90f5\u4ef6\u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_yellow/translations/el.json b/homeassistant/components/homeassistant_yellow/translations/el.json index 8d2d20b4459e74..74343ebb583ee7 100644 --- a/homeassistant/components/homeassistant_yellow/translations/el.json +++ b/homeassistant/components/homeassistant_yellow/translations/el.json @@ -9,6 +9,9 @@ "not_hassio": "\u039f\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03bf\u03cd\u03bd \u03bc\u03cc\u03bd\u03bf \u03c3\u03b5 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 HassOS.", "zha_migration_failed": "\u0397 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 ZHA \u03b4\u03b5\u03bd \u03c0\u03ad\u03c4\u03c5\u03c7\u03b5." }, + "error": { + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, "progress": { "install_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03b1\u03c1\u03ba\u03b5\u03c4\u03ac \u03bb\u03b5\u03c0\u03c4\u03ac.", "start_addon": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c0\u03b5\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c4\u03b5 \u03bc\u03ad\u03c7\u03c1\u03b9 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 Silicon Labs Multiprotocol. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03b1\u03c1\u03ba\u03ad\u03c3\u03b5\u03b9 \u03bc\u03b5\u03c1\u03b9\u03ba\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1." diff --git a/homeassistant/components/knx/translations/uk.json b/homeassistant/components/knx/translations/uk.json new file mode 100644 index 00000000000000..b0b4687777aaf9 --- /dev/null +++ b/homeassistant/components/knx/translations/uk.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b\u044c" + } + } + }, + "options": { + "step": { + "communication_settings": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0432'\u044f\u0437\u043a\u0443" + }, + "connection_type": { + "title": "\u0417'\u0454\u0434\u043d\u0430\u043d\u043d\u044f KNX" + }, + "manual_tunnel": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0443\u043d\u0435\u043b\u044e" + }, + "options_init": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f KNX" + }, + "routing": { + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_key_source": { + "title": "KNX IP-Secure" + }, + "secure_knxkeys": { + "title": "\u041a\u043b\u044e\u0447\u043e\u0432\u0438\u0439 \u0444\u0430\u0439\u043b" + }, + "secure_routing_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_tunnel_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0442\u0443\u043d\u0435\u043b\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b\u044c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/uk.json b/homeassistant/components/ld2410_ble/translations/uk.json new file mode 100644 index 00000000000000..ce46791b8cacaa --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "no_devices_found": "\u0423 \u043c\u0435\u0440\u0435\u0436\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", + "no_unconfigured_devices": "\u041d\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "{\u0456\u043c'\u044f}" + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/uk.json b/homeassistant/components/mjpeg/translations/uk.json new file mode 100644 index 00000000000000..d6552a493999d2 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044c", + "invalid_auth": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 MJPEG", + "name": "\u0406\u043c'\u044f", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "still_image_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u044f\u0442\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/uk.json b/homeassistant/components/moon/translations/uk.json new file mode 100644 index 00000000000000..70ae9e72deff14 --- /dev/null +++ b/homeassistant/components/moon/translations/uk.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "phase": { + "state": { + "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/uk.json b/homeassistant/components/nam/translations/uk.json new file mode 100644 index 00000000000000..3c1aee27feb945 --- /dev/null +++ b/homeassistant/components/nam/translations/uk.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "caqi_level": { + "state": { + "very_high": "\u0414\u0443\u0436\u0435 \u0432\u0438\u0441\u043e\u043a\u043e", + "very_low": "\u0414\u0443\u0436\u0435 \u043d\u0438\u0437\u044c\u043a\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json index 93413f9abff057..a7c4de083172de 100644 --- a/homeassistant/components/pi_hole/translations/uk.json +++ b/homeassistant/components/pi_hole/translations/uk.json @@ -4,9 +4,15 @@ "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json index ac62753459b888..746f754a292aea 100644 --- a/homeassistant/components/plugwise/translations/uk.json +++ b/homeassistant/components/plugwise/translations/uk.json @@ -14,5 +14,18 @@ "title": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Plugwise" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u041d\u0456\u0447" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/uk.json b/homeassistant/components/purpleair/translations/uk.json new file mode 100644 index 00000000000000..b874f5222c3d2a --- /dev/null +++ b/homeassistant/components/purpleair/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "by_coordinates": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + } + }, + "choose_sensor": { + "data": { + "sensor_index": "\u0414\u0430\u0442\u0447\u0438\u043a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/el.json b/homeassistant/components/rainbird/translations/el.json new file mode 100644 index 00000000000000..71ccdf1fa4b048 --- /dev/null +++ b/homeassistant/components/rainbird/translations/el.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 LNK WiFi \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Rain Bird.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Rain Bird \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2023.4. \n\n \u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03c9\u03c3\u03c4\u03cc\u03c3\u03bf \u03bf\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c7\u03c1\u03cc\u03bd\u03bf\u03b9 \u03ac\u03c1\u03b4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03ac \u03b6\u03ce\u03bd\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03ac\u03c1\u03b4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03bb\u03b5\u03c0\u03c4\u03ac" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/et.json b/homeassistant/components/rainbird/translations/et.json new file mode 100644 index 00000000000000..df2ba8d6f03b31 --- /dev/null +++ b/homeassistant/components/rainbird/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "timeout_connect": "\u00dchenduse ajal\u00f5pp" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na" + }, + "description": "Sisesta oma Rain Birdi seadme LNK WiFi mooduli teave.", + "title": "Seadista Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Rain Birdi konfigureerimine failis configuration.yaml eemaldatakse rakendusest Home Assistant 2023.4. \n\n Teie konfiguratsioon imporditi kasutajaliidesesse automaatselt, kuid vaikimisi tsoonip\u00f5hiseid niisutusaegu enam ei toetata. Selle probleemi lahendamiseks eemaldage Rain Bird YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", + "title": "Rain Birdi YAML-konfiguratsioon eemaldatakse" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Vaikimisi kastmisaeg minutites" + }, + "title": "Seadista Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/ru.json b/homeassistant/components/rainbird/translations/ru.json new file mode 100644 index 00000000000000..300bb17f10d3a9 --- /dev/null +++ b/homeassistant/components/rainbird/translations/ru.json @@ -0,0 +1,34 @@ +{ + "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.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043c\u043e\u0434\u0443\u043b\u0435 LNK WiFi \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Rain Bird.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Rain Bird \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2023.4.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0431\u044b\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u043e\u0434\u043d\u0430\u043a\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0438\u0432\u0430 \u043a\u0430\u0436\u0434\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML Rain Bird \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Rain Bird \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u0412\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0438\u0432\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/sk.json b/homeassistant/components/rainbird/translations/sk.json new file mode 100644 index 00000000000000..8b565d747e7014 --- /dev/null +++ b/homeassistant/components/rainbird/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo" + }, + "description": "Zadajte inform\u00e1cie o module WiFi LNK pre va\u0161e zariadenie Rain Bird.", + "title": "Konfigur\u00e1cia Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia Rain Bird v s\u00fabore configuration.yaml sa odstra\u0148uje z Home Assistant 2023.4. \n\n Va\u0161a konfigur\u00e1cia bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky, av\u0161ak predvolen\u00e9 \u010dasy zavla\u017eovania pre jednotliv\u00e9 z\u00f3ny u\u017e nie s\u00fa podporovan\u00e9. Odstr\u00e1\u0148te konfigur\u00e1ciu Rain Bird YAML zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia Rain Bird YAML sa odstra\u0148uje" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Predvolen\u00fd \u010das zavla\u017eovania v min\u00fatach" + }, + "title": "Nakonfigurujte Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/uk.json b/homeassistant/components/rainbird/translations/uk.json new file mode 100644 index 00000000000000..44f3286a1fd882 --- /dev/null +++ b/homeassistant/components/rainbird/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Rain Bird" + } + } + }, + "options": { + "step": { + "init": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/zh-Hant.json b/homeassistant/components/rainbird/translations/zh-Hant.json new file mode 100644 index 00000000000000..3f1dbaec5cf88c --- /dev/null +++ b/homeassistant/components/rainbird/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u8f38\u5165 Rain Bird \u88dd\u7f6e\u4e0a\u7684 LNK WiFi \u6a21\u7d44\u8cc7\u8a0a\u3002", + "title": "\u8a2d\u5b9a Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Rain Bird \u5373\u5c07\u65bc Home Assistant 2023.4 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\uff0c\u4f46\u662f\u9810\u8a2d\u6bcf\u5340\u704c\u6e89\u6642\u9593\u5c07\u4e0d\u518d\u652f\u63f4\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Rain Bird YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Rain Bird YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u9810\u8a2d\u704c\u6e89\u6642\u9593\uff08\u5206\uff09" + }, + "title": "\u8a2d\u5b9a Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/uk.json b/homeassistant/components/reolink/translations/uk.json new file mode 100644 index 00000000000000..bd103582e8a96e --- /dev/null +++ b/homeassistant/components/reolink/translations/uk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "api_error": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 API: {error}", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430: {error}" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "use_https": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HTTPS", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/uk.json b/homeassistant/components/ruuvi_gateway/translations/uk.json new file mode 100644 index 00000000000000..07f1cf45f89102 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/uk.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/uk.json b/homeassistant/components/season/translations/uk.json new file mode 100644 index 00000000000000..cda35222580032 --- /dev/null +++ b/homeassistant/components/season/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "type": "\u0412\u0438\u0434 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0440\u0438 \u0440\u043e\u043a\u0443" + } + } + } + }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "\u041e\u0441\u0456\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0456\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } + } + } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0435\u0437\u043e\u043d\u0443 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e.\n\n\u0412\u0430\u0448\u0430 \u043d\u0430\u044f\u0432\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u043d\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f Home Assistant.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432\u2019\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0441\u0435\u0437\u043e\u043d\u0443 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/uk.json b/homeassistant/components/sensibo/translations/uk.json new file mode 100644 index 00000000000000..b37f57d197bb6e --- /dev/null +++ b/homeassistant/components/sensibo/translations/uk.json @@ -0,0 +1,12 @@ +{ + "entity": { + "select": { + "light": { + "state": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sk.json b/homeassistant/components/sfr_box/translations/sk.json index 329dd18342a1bf..898ccaf555d6c3 100644 --- a/homeassistant/components/sfr_box/translations/sk.json +++ b/homeassistant/components/sfr_box/translations/sk.json @@ -23,6 +23,7 @@ "loss_of_signal": "Strata sign\u00e1lu", "loss_of_signal_quality": "Strata kvality sign\u00e1lu", "no_defect": "Bez defektu", + "of_frame": "Of Frame", "unknown": "Nezn\u00e1me" } }, diff --git a/homeassistant/components/sfr_box/translations/uk.json b/homeassistant/components/sfr_box/translations/uk.json new file mode 100644 index 00000000000000..b5879b4474b5fb --- /dev/null +++ b/homeassistant/components/sfr_box/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "entity": { + "sensor": { + "training": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/ca.json b/homeassistant/components/starlink/translations/ca.json new file mode 100644 index 00000000000000..63b5168b1331be --- /dev/null +++ b/homeassistant/components/starlink/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/el.json b/homeassistant/components/starlink/translations/el.json new file mode 100644 index 00000000000000..1dad5fb0d6d99a --- /dev/null +++ b/homeassistant/components/starlink/translations/el.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/et.json b/homeassistant/components/starlink/translations/et.json new file mode 100644 index 00000000000000..93281ec4ba2a9e --- /dev/null +++ b/homeassistant/components/starlink/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/ru.json b/homeassistant/components/starlink/translations/ru.json new file mode 100644 index 00000000000000..3f5880516ecb0f --- /dev/null +++ b/homeassistant/components/starlink/translations/ru.json @@ -0,0 +1,17 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/sk.json b/homeassistant/components/starlink/translations/sk.json new file mode 100644 index 00000000000000..0283ec1411d7e7 --- /dev/null +++ b/homeassistant/components/starlink/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/uk.json b/homeassistant/components/starlink/translations/uk.json new file mode 100644 index 00000000000000..255191977047a6 --- /dev/null +++ b/homeassistant/components/starlink/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0410\u0434\u0440\u0435\u0441\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/zh-Hant.json b/homeassistant/components/starlink/translations/zh-Hant.json new file mode 100644 index 00000000000000..e59404c8c7dde3 --- /dev/null +++ b/homeassistant/components/starlink/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/uk.json b/homeassistant/components/switchbot/translations/uk.json new file mode 100644 index 00000000000000..aaecd6b9ea5595 --- /dev/null +++ b/homeassistant/components/switchbot/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "encryption_key_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456", + "key_id_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456" + }, + "step": { + "lock_auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e\u0434\u0430\u0442\u043a\u0430 SwitchBot. \u0426\u0456 \u0434\u0430\u043d\u0456 \u043d\u0435 \u0437\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u043c\u043a\u0456\u0432. \u0406\u043c\u0435\u043d\u0430 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432 \u0456 \u043f\u0430\u0440\u043e\u043b\u0456 \u0447\u0443\u0442\u043b\u0438\u0432\u0456 \u0434\u043e \u0440\u0435\u0433\u0456\u0441\u0442\u0440\u0443." + }, + "lock_chose_method": { + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0435\u0442\u043e\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0434\u0435\u0442\u0430\u043b\u0456 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457.", + "menu_options": { + "lock_auth": "\u041b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e\u0434\u0430\u0442\u043a\u0430 SwitchBot", + "lock_key": "\u0411\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f", + "key_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/he.json b/homeassistant/components/vacuum/translations/he.json index 82e0d073406fa9..622e12455914a5 100644 --- a/homeassistant/components/vacuum/translations/he.json +++ b/homeassistant/components/vacuum/translations/he.json @@ -6,17 +6,17 @@ }, "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" + "is_docked": "{entity_name} \u05d1\u05ea\u05d7\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" + "cleaning": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05e7\u05d5\u05ea", + "docked": "{entity_name} \u05d7\u05d6\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" } }, "state": { "_": { "cleaning": "\u05de\u05e0\u05e7\u05d4", - "docked": "\u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4", + "docked": "\u05d1\u05ea\u05d7\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4", "error": "\u05e9\u05d2\u05d9\u05d0\u05d4", "idle": "\u05de\u05de\u05ea\u05d9\u05df", "off": "\u05db\u05d1\u05d5\u05d9", diff --git a/homeassistant/components/zeversolar/translations/uk.json b/homeassistant/components/zeversolar/translations/uk.json new file mode 100644 index 00000000000000..07f1cf45f89102 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/uk.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/uk.json b/homeassistant/components/zodiac/translations/uk.json new file mode 100644 index 00000000000000..4bcc74493452a1 --- /dev/null +++ b/homeassistant/components/zodiac/translations/uk.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "sign": { + "state": { + "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433" + } + } + } + } +} \ No newline at end of file From e7b13cd304c86efbdcf4e9727c08a892f03d48db Mon Sep 17 00:00:00 2001 From: eMerzh Date: Mon, 9 Jan 2023 02:09:37 +0100 Subject: [PATCH 22/35] Add missing context in homewizard assistant error (#85397) --- homeassistant/components/homewizard/config_flow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 60fa4b2451e4ce..3ae2d5fba17e19 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -160,6 +160,11 @@ async def async_step_discovery_confirm( return self.async_show_form( step_id="discovery_confirm", errors={"base": ex.error_code}, + description_placeholders={ + CONF_PRODUCT_TYPE: cast(str, self.config[CONF_PRODUCT_TYPE]), + CONF_SERIAL: cast(str, self.config[CONF_SERIAL]), + CONF_IP_ADDRESS: cast(str, self.config[CONF_IP_ADDRESS]), + }, ) return self.async_create_entry( From f3808fe43535bf638ddfeb9d838f7280e1bb9c95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 Jan 2023 15:09:49 -1000 Subject: [PATCH 23/35] Bump aioesphomeapi to 13.0.4 (#85406) bugfix for protobuf not accepting bytearray changelog: https://github.com/esphome/aioesphomeapi/compare/v13.0.3...v13.0.4 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 1410d3956fa635..95b23befcccb68 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==13.0.3"], + "requirements": ["aioesphomeapi==13.0.4"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index de73fa309f75e2..4aa2deff3baba0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -159,7 +159,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.3 +aioesphomeapi==13.0.4 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58687a00dd83e3..2dbbc254f3c630 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioecowitt==2022.11.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.3 +aioesphomeapi==13.0.4 # homeassistant.components.flo aioflo==2021.11.0 From e197c19e84ab8e0f03eb65dad06b101361174f06 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 8 Jan 2023 17:16:00 -0800 Subject: [PATCH 24/35] Google Assistant SDK: support Korean and Japanese (#85419) * Google Assistant SDK: support Korean and Japanese * Fix Korean and Japanese broadcast commands --- .../components/google_assistant_sdk/const.py | 2 + .../google_assistant_sdk/helpers.py | 2 + .../components/google_assistant_sdk/notify.py | 18 ++-- .../google_assistant_sdk/test_notify.py | 86 ++++++++++++------- 4 files changed, 68 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index acd9a40534337e..8458145caace9d 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -20,5 +20,7 @@ "fr-CA", "fr-FR", "it-IT", + "ja-JP", + "ko-KR", "pt-BR", ] diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 15e325f10c1736..e2d704a917a7f7 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -22,6 +22,8 @@ "es": "es-ES", "fr": "fr-FR", "it": "it-IT", + "ja": "ja-JP", + "ko": "ko-KR", "pt": "pt-BR", } diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index 3872a1df2a39b6..245ea935d461b5 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -13,12 +13,14 @@ # https://support.google.com/assistant/answer/9071582?hl=en LANG_TO_BROADCAST_COMMAND = { - "en": ("broadcast", "broadcast to"), - "de": ("Nachricht an alle", "Nachricht an alle an"), - "es": ("Anuncia", "Anuncia en"), - "fr": ("Diffuse", "Diffuse dans"), - "it": ("Trasmetti", "Trasmetti in"), - "pt": ("Transmite", "Transmite para"), + "en": ("broadcast {0}", "broadcast to {1} {0}"), + "de": ("Nachricht an alle {0}", "Nachricht an alle an {1} {0}"), + "es": ("Anuncia {0}", "Anuncia en {1} {0}"), + "fr": ("Diffuse {0}", "Diffuse dans {1} {0}"), + "it": ("Trasmetti {0}", "Trasmetti in {1} {0}"), + "ja": ("{0}とほうそうして", "{0}と{1}にブロードキャストして"), + "ko": ("{0} 라고 방송해 줘", "{0} 라고 {1}에 방송해 줘"), + "pt": ("Transmite {0}", "Transmite para {1} {0}"), } @@ -62,10 +64,10 @@ async def async_send_message(self, message: str = "", **kwargs: Any) -> None: commands = [] targets = kwargs.get(ATTR_TARGET) if not targets: - commands.append(f"{broadcast_commands(language_code)[0]} {message}") + commands.append(broadcast_commands(language_code)[0].format(message)) else: for target in targets: commands.append( - f"{broadcast_commands(language_code)[1]} {target} {message}" + broadcast_commands(language_code)[1].format(message, target) ) await async_send_text_commands(commands, self.hass) diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 5a2d11b861beb3..ea660f921dd0af 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -1,6 +1,8 @@ """Tests for the Google Assistant notify.""" from unittest.mock import call, patch +import pytest + from homeassistant.components import notify from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES @@ -10,14 +12,29 @@ from .conftest import ComponentSetup, ExpectedCredentials +@pytest.mark.parametrize( + "language_code,message,expected_command", + [ + ("en-US", "Dinner is served", "broadcast Dinner is served"), + ("es-ES", "La cena está en la mesa", "Anuncia La cena está en la mesa"), + ("ko-KR", "저녁 식사가 준비됐어요", "저녁 식사가 준비됐어요 라고 방송해 줘"), + ("ja-JP", "晩ご飯できたよ", "晩ご飯できたよとほうそうして"), + ], + ids=["english", "spanish", "korean", "japanese"], +) async def test_broadcast_no_targets( - hass: HomeAssistant, setup_integration: ComponentSetup + hass: HomeAssistant, + setup_integration: ComponentSetup, + language_code: str, + message: str, + expected_command: str, ) -> None: """Test broadcast to all.""" await setup_integration() - message = "time for dinner" - expected_command = "broadcast time for dinner" + entry = hass.config_entries.async_entries(DOMAIN)[0] + entry.options = {"language_code": language_code} + with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" ) as mock_text_assistant: @@ -27,19 +44,44 @@ async def test_broadcast_no_targets( {notify.ATTR_MESSAGE: message}, ) await hass.async_block_till_done() - mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "en-US") + mock_text_assistant.assert_called_once_with(ExpectedCredentials(), language_code) mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) +@pytest.mark.parametrize( + "language_code,message,target,expected_command", + [ + ( + "en-US", + "it's time for homework", + "living room", + "broadcast to living room it's time for homework", + ), + ( + "es-ES", + "Es hora de hacer los deberes", + "el salón", + "Anuncia en el salón Es hora de hacer los deberes", + ), + ("ko-KR", "숙제할 시간이야", "거실", "숙제할 시간이야 라고 거실에 방송해 줘"), + ("ja-JP", "宿題の時間だよ", "リビング", "宿題の時間だよとリビングにブロードキャストして"), + ], + ids=["english", "spanish", "korean", "japanese"], +) async def test_broadcast_one_target( - hass: HomeAssistant, setup_integration: ComponentSetup + hass: HomeAssistant, + setup_integration: ComponentSetup, + language_code: str, + message: str, + target: str, + expected_command: str, ) -> None: """Test broadcast to one target.""" await setup_integration() - message = "time for dinner" - target = "basement" - expected_command = "broadcast to basement time for dinner" + entry = hass.config_entries.async_entries(DOMAIN)[0] + entry.options = {"language_code": language_code} + with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", return_value=["text_response", None], @@ -98,30 +140,6 @@ async def test_broadcast_empty_message( mock_assist_call.assert_not_called() -async def test_broadcast_spanish( - hass: HomeAssistant, setup_integration: ComponentSetup -) -> None: - """Test broadcast in Spanish.""" - await setup_integration() - - entry = hass.config_entries.async_entries(DOMAIN)[0] - entry.options = {"language_code": "es-ES"} - - message = "comida" - expected_command = "Anuncia comida" - with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" - ) as mock_text_assistant: - await hass.services.async_call( - notify.DOMAIN, - DOMAIN, - {notify.ATTR_MESSAGE: message}, - ) - await hass.async_block_till_done() - mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "es-ES") - mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) - - def test_broadcast_language_mapping( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: @@ -131,4 +149,8 @@ def test_broadcast_language_mapping( assert cmds assert len(cmds) == 2 assert cmds[0] + assert "{0}" in cmds[0] + assert "{1}" not in cmds[0] assert cmds[1] + assert "{0}" in cmds[1] + assert "{1}" in cmds[1] From b1db861f0f01c2188e72bb3fa74aca855b58cb48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 9 Jan 2023 03:17:39 +0200 Subject: [PATCH 25/35] Upgrade RestrictedPython to 6.0 (#85426) Required for Python 3.11. https://github.com/zopefoundation/RestrictedPython/blob/6.0/CHANGES.rst#60-2022-11-03 --- homeassistant/components/python_script/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 2bc2763e777667..586873c2c9c799 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -2,7 +2,7 @@ "domain": "python_script", "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": ["restrictedpython==5.2"], + "requirements": ["restrictedpython==6.0"], "codeowners": [], "quality_scale": "internal", "loggers": ["RestrictedPython"] diff --git a/requirements_all.txt b/requirements_all.txt index 4aa2deff3baba0..9c3d97760c7e62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2212,7 +2212,7 @@ renault-api==0.1.11 reolink-aio==0.1.3 # homeassistant.components.python_script -restrictedpython==5.2 +restrictedpython==6.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2dbbc254f3c630..02204bb6928ada 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1551,7 +1551,7 @@ renault-api==0.1.11 reolink-aio==0.1.3 # homeassistant.components.python_script -restrictedpython==5.2 +restrictedpython==6.0 # homeassistant.components.rflink rflink==0.0.63 From 07d3ba9387bb4c5fa9dc6b1768d4ccc53a08b419 Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Mon, 9 Jan 2023 04:18:36 +0300 Subject: [PATCH 26/35] Z-Wave.Me: Cover: Fixed calibration errors and add missing is_closed (#85452) * Cover: Fixed calibration errors and add missing is_closed * Style * Style * whitespace --- homeassistant/components/zwave_me/cover.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_me/cover.py b/homeassistant/components/zwave_me/cover.py index 5e2fdba86084d4..790cfc6c57409c 100644 --- a/homeassistant/components/zwave_me/cover.py +++ b/homeassistant/components/zwave_me/cover.py @@ -73,8 +73,23 @@ def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. + + Allow small calibration errors (some devices after a long time become not well calibrated) """ - if self.device.level == 99: # Scale max value + if self.device.level > 95: return 100 return self.device.level + + @property + def is_closed(self) -> bool | None: + """Return true if cover is closed. + + None is unknown. + + Allow small calibration errors (some devices after a long time become not well calibrated) + """ + if self.device.level is None: + return None + + return self.device.level < 5 From a6b0b2feb151912079a52d5a175557db509310a8 Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Mon, 9 Jan 2023 04:19:04 +0300 Subject: [PATCH 27/35] Z-Wave.Me integration: Add code owners to receive notifications on github (#85476) * Add code owners to receive notifications on github * fixup! Add code owners to receive notifications on github --- CODEOWNERS | 4 ++-- homeassistant/components/zwave_me/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index dc8503e87e4d3f..348c4b76aa014a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1378,8 +1378,8 @@ build.json @home-assistant/supervisor /homeassistant/components/zoneminder/ @rohankapoorcom /homeassistant/components/zwave_js/ @home-assistant/z-wave /tests/components/zwave_js/ @home-assistant/z-wave -/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me -/tests/components/zwave_me/ @lawfulchaos @Z-Wave-Me +/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS +/tests/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS # Individual files /homeassistant/components/demo/weather.py @fabaff diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 9aeeb7b2a40a1c..9d60d14f274b8d 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -7,5 +7,5 @@ "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, - "codeowners": ["@lawfulchaos", "@Z-Wave-Me"] + "codeowners": ["@lawfulchaos", "@Z-Wave-Me", "@PoltoS"] } From dae499ddd4b2a5b63ef23df5d594b9ad5cb3429b Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 8 Jan 2023 20:45:54 -0600 Subject: [PATCH 28/35] Add network resource button entities to ISY994 and bump PyISY to 3.0.12 (#85429) Co-authored-by: J. Nick Koston --- homeassistant/components/isy994/__init__.py | 34 +++++++--- homeassistant/components/isy994/button.py | 68 +++++++++++++++++-- .../components/isy994/config_flow.py | 13 +++- homeassistant/components/isy994/const.py | 10 +++ homeassistant/components/isy994/entity.py | 6 +- homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/isy994/sensor.py | 3 +- homeassistant/components/isy994/services.py | 35 +++++++--- homeassistant/components/isy994/services.yaml | 4 +- homeassistant/components/isy994/util.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 12 files changed, 146 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index dec7a3874432dd..b075dccc6c4ebc 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -7,6 +7,7 @@ from aiohttp import CookieJar import async_timeout from pyisy import ISY, ISYConnectionError, ISYInvalidAuthError, ISYResponseParseError +from pyisy.constants import PROTO_NETWORK_RESOURCE import voluptuous as vol from homeassistant import config_entries @@ -38,9 +39,19 @@ ISY994_NODES, ISY994_PROGRAMS, ISY994_VARIABLES, + ISY_CONF_FIRMWARE, + ISY_CONF_MODEL, + ISY_CONF_NAME, + ISY_CONF_NETWORKING, + ISY_CONF_UUID, + ISY_CONN_ADDRESS, + ISY_CONN_PORT, + ISY_CONN_TLS, MANUFACTURER, PLATFORMS, PROGRAM_PLATFORMS, + SCHEME_HTTP, + SCHEME_HTTPS, SENSOR_AUX, ) from .helpers import _categorize_nodes, _categorize_programs @@ -122,7 +133,7 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id] = {} hass_isy_data = hass.data[DOMAIN][entry.entry_id] - hass_isy_data[ISY994_NODES] = {SENSOR_AUX: []} + hass_isy_data[ISY994_NODES] = {SENSOR_AUX: [], PROTO_NETWORK_RESOURCE: []} for platform in PLATFORMS: hass_isy_data[ISY994_NODES][platform] = [] @@ -148,13 +159,13 @@ async def async_setup_entry( CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING ) - if host.scheme == "http": + if host.scheme == SCHEME_HTTP: https = False port = host.port or 80 session = aiohttp_client.async_create_clientsession( hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) - elif host.scheme == "https": + elif host.scheme == SCHEME_HTTPS: https = True port = host.port or 443 session = aiohttp_client.async_get_clientsession(hass) @@ -207,6 +218,9 @@ async def async_setup_entry( hass_isy_data[ISY994_VARIABLES].append( (isy.variables[vtype][vid], variable_identifier in vname) ) + if isy.configuration[ISY_CONF_NETWORKING]: + for resource in isy.networking.nobjs: + hass_isy_data[ISY994_NODES][PROTO_NETWORK_RESOURCE].append(resource) # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs _LOGGER.info(repr(isy.clock)) @@ -267,8 +281,8 @@ def _async_import_options_from_data_if_missing( def _async_isy_to_configuration_url(isy: ISY) -> str: """Extract the configuration url from the isy.""" connection_info = isy.conn.connection_info - proto = "https" if "tls" in connection_info else "http" - return f"{proto}://{connection_info['addr']}:{connection_info['port']}" + proto = SCHEME_HTTPS if ISY_CONN_TLS in connection_info else SCHEME_HTTP + return f"{proto}://{connection_info[ISY_CONN_ADDRESS]}:{connection_info[ISY_CONN_PORT]}" @callback @@ -279,12 +293,12 @@ def _async_get_or_create_isy_device_in_registry( url = _async_isy_to_configuration_url(isy) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration["uuid"])}, - identifiers={(DOMAIN, isy.configuration["uuid"])}, + connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration[ISY_CONF_UUID])}, + identifiers={(DOMAIN, isy.configuration[ISY_CONF_UUID])}, manufacturer=MANUFACTURER, - name=isy.configuration["name"], - model=isy.configuration["model"], - sw_version=isy.configuration["firmware"], + name=isy.configuration[ISY_CONF_NAME], + model=isy.configuration[ISY_CONF_MODEL], + sw_version=isy.configuration[ISY_CONF_FIRMWARE], configuration_url=url, ) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py index 0325c501d63f1e..7dbddafda24e7b 100644 --- a/homeassistant/components/isy994/button.py +++ b/homeassistant/components/isy994/button.py @@ -2,17 +2,29 @@ from __future__ import annotations from pyisy import ISY -from pyisy.constants import PROTO_INSTEON +from pyisy.constants import PROTO_INSTEON, PROTO_NETWORK_RESOURCE from pyisy.nodes import Node from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN as ISY994_DOMAIN, ISY994_ISY, ISY994_NODES +from . import _async_isy_to_configuration_url +from .const import ( + DOMAIN as ISY994_DOMAIN, + ISY994_ISY, + ISY994_NODES, + ISY_CONF_FIRMWARE, + ISY_CONF_MODEL, + ISY_CONF_NAME, + ISY_CONF_NETWORKING, + ISY_CONF_UUID, + MANUFACTURER, +) async def async_setup_entry( @@ -23,13 +35,23 @@ async def async_setup_entry( """Set up ISY/IoX button from config entry.""" hass_isy_data = hass.data[ISY994_DOMAIN][config_entry.entry_id] isy: ISY = hass_isy_data[ISY994_ISY] - uuid = isy.configuration["uuid"] - entities: list[ISYNodeQueryButtonEntity | ISYNodeBeepButtonEntity] = [] - for node in hass_isy_data[ISY994_NODES][Platform.BUTTON]: + uuid = isy.configuration[ISY_CONF_UUID] + entities: list[ + ISYNodeQueryButtonEntity + | ISYNodeBeepButtonEntity + | ISYNetworkResourceButtonEntity + ] = [] + nodes: dict = hass_isy_data[ISY994_NODES] + for node in nodes[Platform.BUTTON]: entities.append(ISYNodeQueryButtonEntity(node, f"{uuid}_{node.address}")) if node.protocol == PROTO_INSTEON: entities.append(ISYNodeBeepButtonEntity(node, f"{uuid}_{node.address}")) + for node in nodes[PROTO_NETWORK_RESOURCE]: + entities.append( + ISYNetworkResourceButtonEntity(node, f"{uuid}_{PROTO_NETWORK_RESOURCE}") + ) + # Add entity to query full system entities.append(ISYNodeQueryButtonEntity(isy, uuid)) @@ -80,3 +102,39 @@ def __init__(self, node: Node, base_unique_id: str) -> None: async def async_press(self) -> None: """Press the button.""" await self._node.beep() + + +class ISYNetworkResourceButtonEntity(ButtonEntity): + """Representation of an ISY/IoX Network Resource button entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__(self, node: Node, base_unique_id: str) -> None: + """Initialize an ISY network resource button entity.""" + self._node = node + + # Entity class attributes + self._attr_name = node.name + self._attr_unique_id = f"{base_unique_id}_{node.address}" + url = _async_isy_to_configuration_url(node.isy) + config = node.isy.configuration + self._attr_device_info = DeviceInfo( + identifiers={ + ( + ISY994_DOMAIN, + f"{config[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}", + ) + }, + manufacturer=MANUFACTURER, + name=f"{config[ISY_CONF_NAME]} {ISY_CONF_NETWORKING}", + model=config[ISY_CONF_MODEL], + sw_version=config[ISY_CONF_FIRMWARE], + configuration_url=url, + via_device=(ISY994_DOMAIN, config[ISY_CONF_UUID]), + entry_type=DeviceEntryType.SERVICE, + ) + + async def async_press(self) -> None: + """Press the button.""" + await self._node.run() diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 3c058689bf8490..908bf710bf436c 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -34,6 +34,8 @@ DOMAIN, HTTP_PORT, HTTPS_PORT, + ISY_CONF_NAME, + ISY_CONF_UUID, ISY_URL_POSTFIX, SCHEME_HTTP, SCHEME_HTTPS, @@ -106,11 +108,14 @@ async def validate_input( isy_conf = Configuration(xml=isy_conf_xml) except ISYResponseParseError as error: raise CannotConnect from error - if not isy_conf or "name" not in isy_conf or not isy_conf["name"]: + if not isy_conf or ISY_CONF_NAME not in isy_conf or not isy_conf[ISY_CONF_NAME]: raise CannotConnect # Return info that you want to store in the config entry. - return {"title": f"{isy_conf['name']} ({host.hostname})", "uuid": isy_conf["uuid"]} + return { + "title": f"{isy_conf[ISY_CONF_NAME]} ({host.hostname})", + ISY_CONF_UUID: isy_conf[ISY_CONF_UUID], + } class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -151,7 +156,9 @@ async def async_step_user( errors["base"] = "unknown" if not errors: - await self.async_set_unique_id(info["uuid"], raise_on_progress=False) + await self.async_set_unique_id( + info[ISY_CONF_UUID], raise_on_progress=False + ) self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 1e9733395f4302..3df11f078eacab 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -105,6 +105,16 @@ ISY994_PROGRAMS = "isy994_programs" ISY994_VARIABLES = "isy994_variables" +ISY_CONF_NETWORKING = "Networking Module" +ISY_CONF_UUID = "uuid" +ISY_CONF_NAME = "name" +ISY_CONF_MODEL = "model" +ISY_CONF_FIRMWARE = "firmware" + +ISY_CONN_PORT = "port" +ISY_CONN_ADDRESS = "addr" +ISY_CONN_TLS = "tls" + FILTER_UOM = "uom" FILTER_STATES = "states" FILTER_NODE_DEF_ID = "node_def_id" diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index f5633167cd10ba..144ec016d22d33 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from . import _async_isy_to_configuration_url -from .const import DOMAIN +from .const import DOMAIN, ISY_CONF_UUID class ISYEntity(Entity): @@ -73,7 +73,7 @@ def async_on_control(self, event: NodeProperty) -> None: def device_info(self) -> DeviceInfo | None: """Return the device_info of the device.""" isy = self._node.isy - uuid = isy.configuration["uuid"] + uuid = isy.configuration[ISY_CONF_UUID] node = self._node url = _async_isy_to_configuration_url(isy) @@ -127,7 +127,7 @@ def device_info(self) -> DeviceInfo | None: def unique_id(self) -> str | None: """Get the unique identifier of the device.""" if hasattr(self._node, "address"): - return f"{self._node.isy.configuration['uuid']}_{self._node.address}" + return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}" return None @property diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 8370f4ace481df..4cab67c7d96559 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.11"], + "requirements": ["pyisy==3.0.12"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 6efe34c1d87c49..5840562c5fbed5 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -35,6 +35,7 @@ _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, + ISY_CONF_UUID, SENSOR_AUX, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, @@ -251,7 +252,7 @@ def unique_id(self) -> str | None: """Get the unique identifier of the device and aux sensor.""" if not hasattr(self._node, "address"): return None - return f"{self._node.isy.configuration['uuid']}_{self._node.address}_{self._control}" + return f"{self._node.isy.configuration[ISY_CONF_UUID]}_{self._node.address}_{self._control}" @property def name(self) -> str: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 0ab328484e2797..dc4284da8dc9c6 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -3,7 +3,7 @@ from typing import Any -from pyisy.constants import COMMAND_FRIENDLY_NAME +from pyisy.constants import COMMAND_FRIENDLY_NAME, PROTO_NETWORK_RESOURCE import voluptuous as vol from homeassistant.const import ( @@ -23,7 +23,14 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import entity_service_call -from .const import _LOGGER, DOMAIN, ISY994_ISY +from .const import ( + _LOGGER, + DOMAIN, + ISY994_ISY, + ISY_CONF_NAME, + ISY_CONF_NETWORKING, + ISY_CONF_UUID, +) from .util import unique_ids_for_config_entry_id ISY_CONF_UUID = "uuid" # TODO: Remove and import when PR#85429 is merged @@ -196,7 +203,7 @@ async def async_system_query_service_handler(service: ServiceCall) -> None: _LOGGER.debug( "Requesting query of device %s on ISY %s", address, - isy.configuration["uuid"], + isy.configuration[ISY_CONF_UUID], ) await isy.query(address) async_log_deprecated_service_call( @@ -206,13 +213,13 @@ async def async_system_query_service_handler(service: ServiceCall) -> None: alternate_target=entity_registry.async_get_entity_id( Platform.BUTTON, DOMAIN, - f"{isy.configuration['uuid']}_{address}_query", + f"{isy.configuration[ISY_CONF_UUID]}_{address}_query", ), breaks_in_ha_version="2023.5.0", ) return _LOGGER.debug( - "Requesting system query of ISY %s", isy.configuration["uuid"] + "Requesting system query of ISY %s", isy.configuration[ISY_CONF_UUID] ) await isy.query() async_log_deprecated_service_call( @@ -220,7 +227,7 @@ async def async_system_query_service_handler(service: ServiceCall) -> None: call=service, alternate_service="button.press", alternate_target=entity_registry.async_get_entity_id( - Platform.BUTTON, DOMAIN, f"{isy.configuration['uuid']}_query" + Platform.BUTTON, DOMAIN, f"{isy.configuration[ISY_CONF_UUID]}_query" ), breaks_in_ha_version="2023.5.0", ) @@ -233,9 +240,9 @@ async def async_run_network_resource_service_handler(service: ServiceCall) -> No for config_entry_id in hass.data[DOMAIN]: isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + if isy_name and isy_name != isy.configuration[ISY_CONF_NAME]: continue - if not hasattr(isy, "networking") or isy.networking is None: + if isy.networking is None or not isy.configuration[ISY_CONF_NETWORKING]: continue command = None if address: @@ -244,6 +251,18 @@ async def async_run_network_resource_service_handler(service: ServiceCall) -> No command = isy.networking.get_by_name(name) if command is not None: await command.run() + entity_registry = er.async_get(hass) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="button.press", + alternate_target=entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{isy.configuration[ISY_CONF_UUID]}_{PROTO_NETWORK_RESOURCE}_{address}", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.error( "Could not run network resource command; not found or enabled on the ISY" diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 0e82df2b3560b5..c9daa828970542 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -267,8 +267,8 @@ send_program_command: selector: text: run_network_resource: - name: Run network resource - description: Run a network resource on the ISY. + name: Run network resource (Deprecated) + description: "Run a network resource on the ISY. Deprecated: Use Network Resource button entity." fields: address: name: Address diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py index 196801c58cea34..45bf10cc1bd6fd 100644 --- a/homeassistant/components/isy994/util.py +++ b/homeassistant/components/isy994/util.py @@ -9,6 +9,7 @@ ISY994_NODES, ISY994_PROGRAMS, ISY994_VARIABLES, + ISY_CONF_UUID, PLATFORMS, PROGRAM_PLATFORMS, ) @@ -19,7 +20,7 @@ def unique_ids_for_config_entry_id( ) -> set[str]: """Find all the unique ids for a config entry id.""" hass_isy_data = hass.data[DOMAIN][config_entry_id] - uuid = hass_isy_data[ISY994_ISY].configuration["uuid"] + uuid = hass_isy_data[ISY994_ISY].configuration[ISY_CONF_UUID] current_unique_ids: set[str] = {uuid} for platform in PLATFORMS: diff --git a/requirements_all.txt b/requirements_all.txt index 9c3d97760c7e62..cb210058118aa2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1693,7 +1693,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.11 +pyisy==3.0.12 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02204bb6928ada..f953ab2452e80c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1206,7 +1206,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.11 +pyisy==3.0.12 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 2320afec5a67f530d45faca70b6aaf55d004683d Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 8 Jan 2023 21:59:21 -0500 Subject: [PATCH 29/35] Bump pyunifiprotect to 4.6.0 (#85483) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index c7259356b66dd1..e30818bd42f36f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.5.2", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.6.0", "unifi-discovery==1.1.7"], "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index cb210058118aa2..0b8e6d346352cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2128,7 +2128,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.5.2 +pyunifiprotect==4.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f953ab2452e80c..8eee3ae9af5a0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1494,7 +1494,7 @@ pytrafikverket==0.2.2 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.5.2 +pyunifiprotect==4.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 83b5f267fa2de862318a1b90787d8cb0b2ba44a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 07:01:55 +0100 Subject: [PATCH 30/35] Code styling tweaks to core utils & YAML loader (#85433) Code styling tweaks to core utils --- homeassistant/util/__init__.py | 6 ++-- homeassistant/util/color.py | 12 +++---- homeassistant/util/dt.py | 48 ++++++++++++++++----------- homeassistant/util/json.py | 3 +- homeassistant/util/location.py | 3 +- homeassistant/util/package.py | 4 ++- homeassistant/util/pil.py | 3 +- homeassistant/util/ssl.py | 8 ++--- homeassistant/util/unit_conversion.py | 4 +-- homeassistant/util/unit_system.py | 8 +++-- homeassistant/util/yaml/loader.py | 20 ++++++++--- 11 files changed, 68 insertions(+), 51 deletions(-) diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index eb3dabe75a03c2..19372bd765b054 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -23,8 +23,7 @@ def raise_if_invalid_filename(filename: str) -> None: - """ - Check if a filename is valid. + """Check if a filename is valid. Raises a ValueError if the filename is invalid. """ @@ -33,8 +32,7 @@ def raise_if_invalid_filename(filename: str) -> None: def raise_if_invalid_path(path: str) -> None: - """ - Check if a path is valid. + """Check if a path is valid. Raises a ValueError if the path is invalid. """ diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 3823c0e45bdbac..6ccb7f14ea204f 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -510,8 +510,7 @@ def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, flo def color_temperature_to_rgb( color_temperature_kelvin: float, ) -> tuple[float, float, float]: - """ - Return an RGB color from a color temperature in Kelvin. + """Return an RGB color from a color temperature in Kelvin. This is a rough approximation based on the formula provided by T. Helland http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ @@ -581,8 +580,7 @@ def _white_levels_to_color_temperature( def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float: - """ - Clamp the given color component value between the given min and max values. + """Clamp the given color component value between the given min and max values. The range defined by the minimum and maximum values is inclusive, i.e. given a color_component of 0 and a minimum of 10, the returned value is 10. @@ -644,8 +642,7 @@ def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float: def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: - """ - Find the closest point from P to a line defined by A and B. + """Find the closest point from P to a line defined by A and B. This point will be reproducible by the lamp as it is on the edge of the gamut. @@ -667,8 +664,7 @@ def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: def get_closest_point_to_point( xy_tuple: tuple[float, float], Gamut: GamutType ) -> tuple[float, float]: - """ - Get the closest matching color within the gamut of the light. + """Get the closest matching color within the gamut of the light. Should only be used if the supplied color is outside of the color gamut. """ diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 3e9ae088296ade..26f001236ec275 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -267,8 +267,7 @@ def parse_time(time_str: str) -> dt.time | None: def get_age(date: dt.datetime) -> str: - """ - Take a datetime and return its "age" as a string. + """Take a datetime and return its "age" as a string. The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will @@ -328,7 +327,9 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> lis def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta: """Return the offset when crossing the DST barrier.""" delta = dt.timedelta(hours=24) - return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator] + return (dattim + delta).utcoffset() - ( # type: ignore[operator] + dattim - delta + ).utcoffset() def _lower_bound(arr: list[int], cmp: int) -> int | None: @@ -360,7 +361,8 @@ def find_next_time_expression_time( raise ValueError("Cannot find a next time: Time expression never matches!") while True: - # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + # Reset microseconds and fold; fold (for ambiguous DST times) will be + # handled later. result = now.replace(microsecond=0, fold=0) # Match next second @@ -408,11 +410,12 @@ def find_next_time_expression_time( # -> trigger on the next time that 1. matches the pattern and 2. does exist # for example: # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour - # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) - # instead run at 02:30 the next day + # with pattern "02:30", don't run on 28 mar (such a wall time does not + # exist on this day) instead run at 02:30 the next day - # We solve this edge case by just iterating one second until the result exists - # (max. 3600 operations, which should be fine for an edge case that happens once a year) + # We solve this edge case by just iterating one second until the result + # exists (max. 3600 operations, which should be fine for an edge case that + # happens once a year) now += dt.timedelta(seconds=1) continue @@ -420,29 +423,34 @@ def find_next_time_expression_time( return result # When leaving DST and clocks are turned backward. - # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST - # The logic above does not take into account if a given pattern matches _twice_ - # in a day. - # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + # Then there are wall clock times that are ambiguous i.e. exist with DST and + # without DST. The logic above does not take into account if a given pattern + # matches _twice_ in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned + # backward an hour. if _datetime_ambiguous(result): # `now` and `result` are both ambiguous, so the next match happens # _within_ the current fold. # Examples: - # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 - # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+02:00 + # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+01:00 return result.replace(fold=now.fold) if now.fold == 0: - # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches - # within the fold). - # -> Check if result matches in the next fold. If so, emit that match + # `now` is in the first fold, but result is not ambiguous (meaning it no + # longer matches within the fold). + # -> Check if result matches in the next fold. If so, emit that match - # Turn back the time by the DST offset, effectively run the algorithm on the first fold - # If it matches on the first fold, that means it will also match on the second one. + # Turn back the time by the DST offset, effectively run the algorithm on + # the first fold. If it matches on the first fold, that means it will also + # match on the second one. - # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+01:00 check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index eb71d9da7eb219..a31db4f8d1bdfe 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -124,7 +124,8 @@ def find_paths_unserializable_data( except (ValueError, TypeError): pass - # We convert objects with as_dict to their dict values so we can find bad data inside it + # We convert objects with as_dict to their dict values + # so we can find bad data inside it if hasattr(obj, "as_dict"): desc = obj.__class__.__name__ if isinstance(obj, State): diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index b4d7274ded78e8..407ad3881cd2a5 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -79,8 +79,7 @@ def distance( def vincenty( point1: tuple[float, float], point2: tuple[float, float], miles: bool = False ) -> float | None: - """ - Vincenty formula (inverse method) to calculate the distance. + """Vincenty formula (inverse method) to calculate the distance. Result in kilometers or miles between two points on the surface of a spheroid. diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 49ab3c10f8c772..b67e9923b9c961 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -52,7 +52,9 @@ def is_installed(package: str) -> bool: # was aborted while in progress see # https://github.com/home-assistant/core/issues/47699 if installed_version is None: - _LOGGER.error("Installed version for %s resolved to None", req.project_name) # type: ignore[unreachable] + _LOGGER.error( # type: ignore[unreachable] + "Installed version for %s resolved to None", req.project_name + ) return False return installed_version in req except PackageNotFoundError: diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py index 7caeac15458aa5..068b807cbe51e9 100644 --- a/homeassistant/util/pil.py +++ b/homeassistant/util/pil.py @@ -15,8 +15,7 @@ def draw_box( text: str = "", color: tuple[int, int, int] = (255, 255, 0), ) -> None: - """ - Draw a bounding box on and image. + """Draw a bounding box on and image. The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) where the coordinates are floats in the range [0.0, 1.0] and diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 4f10809ff21172..ffeefe3d2c94ab 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -8,12 +8,12 @@ def client_context() -> ssl.SSLContext: """Return an SSL context for making requests.""" - # Reuse environment variable definition from requests, since it's already a requirement - # If the environment variable has no value, fall back to using certs from certifi package + # Reuse environment variable definition from requests, since it's already a + # requirement. If the environment variable has no value, fall back to using + # certs from certifi package. cafile = environ.get("REQUESTS_CA_BUNDLE", certifi.where()) - context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile) - return context + return ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile) def server_context_modern() -> ssl.SSLContext: diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index f9f4d78899a221..274f13cd0b53aa 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -342,8 +342,8 @@ def convert(cls, value: float, from_unit: str, to_unit: str) -> float: For converting an interval between two temperatures, please use `convert_interval` instead. """ - # We cannot use the implementation from BaseUnitConverter here because the temperature - # units do not use the same floor: 0°C, 0°F and 0K do not align + # We cannot use the implementation from BaseUnitConverter here because the + # temperature units do not use the same floor: 0°C, 0°F and 0K do not align if from_unit == to_unit: return value diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 7aa910e90b2314..194b8d82dbb54c 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -195,7 +195,9 @@ def wind_speed(self, wind_speed: float | None, from_unit: str) -> float: raise TypeError(f"{wind_speed!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return SpeedConverter.convert(wind_speed, from_unit, self.wind_speed_unit) # type: ignore[unreachable] + return SpeedConverter.convert( # type: ignore[unreachable] + wind_speed, from_unit, self.wind_speed_unit + ) def volume(self, volume: float | None, from_unit: str) -> float: """Convert the given volume to this unit system.""" @@ -203,7 +205,9 @@ def volume(self, volume: float | None, from_unit: str) -> float: raise TypeError(f"{volume!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return VolumeConverter.convert(volume, from_unit, self.volume_unit) # type: ignore[unreachable] + return VolumeConverter.convert( # type: ignore[unreachable] + volume, from_unit, self.volume_unit + ) def as_dict(self) -> dict[str, str]: """Convert the unit system to a dictionary.""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 626cf65d1e2c58..6520ca60e81286 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -18,7 +18,9 @@ HAS_C_LOADER = True except ImportError: HAS_C_LOADER = False - from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[assignment] + from yaml import ( # type: ignore[assignment] + SafeLoader as FastestAvailableSafeLoader, + ) from homeassistant.exceptions import HomeAssistantError @@ -132,10 +134,14 @@ def __init__(self, stream: Any, secrets: Secrets | None = None) -> None: super().__init__(stream) self.secrets = secrets - def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: # type: ignore[override] + def compose_node( # type: ignore[override] + self, parent: yaml.nodes.Node, index: int + ) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" last_line: int = self.line - node: yaml.nodes.Node = super().compose_node(parent, index) # type: ignore[assignment] + node: yaml.nodes.Node = super().compose_node( # type: ignore[assignment] + parent, index + ) node.__line__ = last_line + 1 # type: ignore[attr-defined] return node @@ -226,7 +232,9 @@ def _add_reference(obj: _DictT, loader: LoaderType, node: yaml.nodes.Node) -> _D ... -def _add_reference(obj, loader: LoaderType, node: yaml.nodes.Node): # type: ignore[no-untyped-def] +def _add_reference( # type: ignore[no-untyped-def] + obj, loader: LoaderType, node: yaml.nodes.Node +): """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) @@ -337,7 +345,9 @@ def _ordered_dict(loader: LoaderType, node: yaml.nodes.MappingNode) -> OrderedDi fname = loader.get_stream_name() raise yaml.MarkedYAMLError( context=f'invalid key: "{key}"', - context_mark=yaml.Mark(fname, 0, line, -1, None, None), # type: ignore[arg-type] + context_mark=yaml.Mark( + fname, 0, line, -1, None, None # type: ignore[arg-type] + ), ) from exc if key in seen: From edbd3818bf8ae9eefc5655fc83659a8645fef39d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 10:30:14 +0100 Subject: [PATCH 31/35] Bump actions/upload-artifact from 3.1.1 to 3.1.2 (#85489) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 4 ++-- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b251b3d522fdc0..1331d73df11544 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -852,7 +852,7 @@ jobs: -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -954,7 +954,7 @@ jobs: --dburl=mysql://root:password@127.0.0.1/homeassistant-test \ tests/components/recorder - name: Upload coverage artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-mariadb path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 67376235ccbb4e..8311f4dc8ffe72 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -57,13 +57,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: requirements_diff path: ./requirements_diff.txt From bf04f14e5a675830b5d169e14a9ddf4380cc24c0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 9 Jan 2023 10:42:49 +0100 Subject: [PATCH 32/35] Use power factor device class in Fronius integration again (#85495) --- homeassistant/components/fronius/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 53342864da7f09..670551ca7c5f06 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -333,24 +333,28 @@ async def async_setup_entry( SensorEntityDescription( key="power_factor_phase_1", name="Power factor phase 1", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_2", name="Power factor phase 2", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_3", name="Power factor phase 3", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor", name="Power factor", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( From e5a5b77af02d8779894081d58a7fc5fb46d48dcf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jan 2023 11:28:36 +0100 Subject: [PATCH 33/35] Bump actions/cache from 3.2.2 to 3.2.3 (#85488) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1331d73df11544..e7ba3a5d3fbc9c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -176,7 +176,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: venv key: >- @@ -191,7 +191,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -220,7 +220,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -233,7 +233,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -274,7 +274,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -287,7 +287,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -331,7 +331,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -344,7 +344,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -377,7 +377,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -390,7 +390,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -509,7 +509,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: venv key: >- @@ -517,7 +517,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: ${{ env.PIP_CACHE }} key: >- @@ -568,7 +568,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -601,7 +601,7 @@ jobs: check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -635,7 +635,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -680,7 +680,7 @@ jobs: check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -729,7 +729,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -784,7 +784,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -907,7 +907,7 @@ jobs: check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From e625725b807f1da22f083e51bf69cd647a09732b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Jan 2023 11:39:20 +0100 Subject: [PATCH 34/35] Remove invalid Signal Strength device class from NETGEAR (#85510) --- homeassistant/components/netgear/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index a350bfe92656fc..3c4e13748cfda7 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -58,7 +58,6 @@ key="signal", name="signal strength", native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, ), "ssid": SensorEntityDescription( From 366b7fdd531729ebb4ab07d6aa9d1a99e49462fd Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 9 Jan 2023 12:41:47 +0200 Subject: [PATCH 35/35] Add config flow to imap (#74623) * Add config flow to imap fix coverage fix config_flows.py * move coordinator to seperate file, remove name key * update intrgations.json * update requirements_all.txt * fix importing issue_registry * Address comments * Improve handling exceptions on intial connection * exit loop tasks properly * fix timeout * revert async_timeout * Improve entity update handling * ensure we wait for idle to finish * fix typing * Update deprecation period Co-authored-by: Martin Hjelmare --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/components/imap/__init__.py | 55 ++- homeassistant/components/imap/config_flow.py | 136 +++++++ homeassistant/components/imap/const.py | 12 + homeassistant/components/imap/coordinator.py | 104 ++++++ homeassistant/components/imap/errors.py | 11 + homeassistant/components/imap/manifest.json | 4 +- homeassistant/components/imap/sensor.py | 207 +++-------- homeassistant/components/imap/strings.json | 40 ++ .../components/imap/translations/en.json | 40 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/imap/__init__.py | 1 + tests/components/imap/test_config_flow.py | 349 ++++++++++++++++++ 16 files changed, 819 insertions(+), 150 deletions(-) create mode 100644 homeassistant/components/imap/config_flow.py create mode 100644 homeassistant/components/imap/const.py create mode 100644 homeassistant/components/imap/coordinator.py create mode 100644 homeassistant/components/imap/errors.py create mode 100644 homeassistant/components/imap/strings.json create mode 100644 homeassistant/components/imap/translations/en.json create mode 100644 tests/components/imap/__init__.py create mode 100644 tests/components/imap/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 858c1761566dea..0a15bf21223ae2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -561,6 +561,8 @@ omit = homeassistant/components/ifttt/const.py homeassistant/components/iglo/light.py homeassistant/components/ihc/* + homeassistant/components/imap/__init__.py + homeassistant/components/imap/coordinator.py homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py homeassistant/components/incomfort/* diff --git a/CODEOWNERS b/CODEOWNERS index 348c4b76aa014a..42c0186520a16c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -537,6 +537,8 @@ build.json @home-assistant/supervisor /tests/components/image_processing/ @home-assistant/core /homeassistant/components/image_upload/ @home-assistant/core /tests/components/image_upload/ @home-assistant/core +/homeassistant/components/imap/ @engrbm87 +/tests/components/imap/ @engrbm87 /homeassistant/components/incomfort/ @zxdavb /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index d85f295a43e170..7e582aa04d4e55 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -1 +1,54 @@ -"""The imap component.""" +"""The imap integration.""" +from __future__ import annotations + +import asyncio + +from aioimaplib import IMAP4_SSL, AioImapException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) + +from .const import DOMAIN +from .coordinator import ImapDataUpdateCoordinator, connect_to_server +from .errors import InvalidAuth, InvalidFolder + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up imap from a config entry.""" + try: + imap_client: IMAP4_SSL = await connect_to_server(dict(entry.data)) + except InvalidAuth as err: + raise ConfigEntryAuthFailed from err + except InvalidFolder as err: + raise ConfigEntryError("Selected mailbox folder is invalid.") from err + except (asyncio.TimeoutError, AioImapException) as err: + raise ConfigEntryNotReady from err + + coordinator = ImapDataUpdateCoordinator(hass, imap_client) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.shutdown() + return unload_ok diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py new file mode 100644 index 00000000000000..7306d07d06a517 --- /dev/null +++ b/homeassistant/components/imap/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for imap integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +from aioimaplib import AioImapException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + CONF_SERVER, + DEFAULT_PORT, + DOMAIN, +) +from .coordinator import connect_to_server +from .errors import InvalidAuth, InvalidFolder + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SERVER): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_CHARSET, default="utf-8"): str, + vol.Optional(CONF_FOLDER, default="INBOX"): str, + vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + } +) + + +async def validate_input(user_input: dict[str, Any]) -> dict[str, str]: + """Validate user input.""" + errors = {} + + try: + imap_client = await connect_to_server(user_input) + result, lines = await imap_client.search( + user_input[CONF_SEARCH], + charset=user_input[CONF_CHARSET], + ) + + except InvalidAuth: + errors[CONF_USERNAME] = errors[CONF_PASSWORD] = "invalid_auth" + except InvalidFolder: + errors[CONF_FOLDER] = "invalid_folder" + except (asyncio.TimeoutError, AioImapException, ConnectionRefusedError): + errors["base"] = "cannot_connect" + else: + if result != "OK": + if "The specified charset is not supported" in lines[0].decode("utf-8"): + errors[CONF_CHARSET] = "invalid_charset" + else: + errors[CONF_SEARCH] = "invalid_search" + return errors + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for imap.""" + + VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + self._async_abort_entries_match( + { + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_FOLDER: user_input[CONF_FOLDER], + CONF_SEARCH: user_input[CONF_SEARCH], + } + ) + + if not (errors := await validate_input(user_input)): + # To be removed when YAML import is removed + title = user_input.get(CONF_NAME, user_input[CONF_USERNAME]) + + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + 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(import_config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_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, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + assert self._reauth_entry + if user_input is not None: + user_input = {**self._reauth_entry.data, **user_input} + if not (errors := await validate_input(user_input)): + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py new file mode 100644 index 00000000000000..080f7bf676578b --- /dev/null +++ b/homeassistant/components/imap/const.py @@ -0,0 +1,12 @@ +"""Constants for the imap integration.""" + +from typing import Final + +DOMAIN: Final = "imap" + +CONF_SERVER: Final = "server" +CONF_FOLDER: Final = "folder" +CONF_SEARCH: Final = "search" +CONF_CHARSET: Final = "charset" + +DEFAULT_PORT: Final = 993 diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py new file mode 100644 index 00000000000000..8a716fe478634b --- /dev/null +++ b/homeassistant/components/imap/coordinator.py @@ -0,0 +1,104 @@ +"""Coordinator for imag integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from datetime import timedelta +import logging +from typing import Any + +from aioimaplib import AUTH, IMAP4_SSL, SELECTED, AioImapException +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_CHARSET, CONF_FOLDER, CONF_SEARCH, CONF_SERVER, DOMAIN +from .errors import InvalidAuth, InvalidFolder + +_LOGGER = logging.getLogger(__name__) + + +async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL: + """Connect to imap server and return client.""" + client = IMAP4_SSL(data[CONF_SERVER], data[CONF_PORT]) + await client.wait_hello_from_server() + await client.login(data[CONF_USERNAME], data[CONF_PASSWORD]) + if client.protocol.state != AUTH: + raise InvalidAuth + await client.select(data[CONF_FOLDER]) + if client.protocol.state != SELECTED: + raise InvalidFolder + return client + + +class ImapDataUpdateCoordinator(DataUpdateCoordinator[int]): + """Class for imap client.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, imap_client: IMAP4_SSL) -> None: + """Initiate imap client.""" + self.hass = hass + self.imap_client = imap_client + self.support_push = imap_client.has_capability("IDLE") + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=10) if not self.support_push else None, + ) + + async def _async_update_data(self) -> int: + """Update the number of unread emails.""" + try: + if self.imap_client is None: + self.imap_client = await connect_to_server(self.config_entry.data) + except (AioImapException, asyncio.TimeoutError) as err: + raise UpdateFailed(err) from err + + return await self.refresh_email_count() + + async def refresh_email_count(self) -> int: + """Check the number of found emails.""" + try: + await self.imap_client.noop() + result, lines = await self.imap_client.search( + self.config_entry.data[CONF_SEARCH], + charset=self.config_entry.data[CONF_CHARSET], + ) + except (AioImapException, asyncio.TimeoutError) as err: + raise UpdateFailed(err) from err + + if result != "OK": + raise UpdateFailed( + f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}" + ) + if self.support_push: + self.hass.async_create_task(self.async_wait_server_push()) + return len(lines[0].split()) + + async def async_wait_server_push(self) -> None: + """Wait for data push from server.""" + try: + idle: asyncio.Future = await self.imap_client.idle_start() + await self.imap_client.wait_server_push() + self.imap_client.idle_done() + async with async_timeout.timeout(10): + await idle + + except (AioImapException, asyncio.TimeoutError): + _LOGGER.warning( + "Lost %s (will attempt to reconnect)", + self.config_entry.data[CONF_SERVER], + ) + self.imap_client = None + await self.async_request_refresh() + + async def shutdown(self, *_) -> None: + """Close resources.""" + if self.imap_client: + await self.imap_client.stop_wait_server_push() + await self.imap_client.logout() diff --git a/homeassistant/components/imap/errors.py b/homeassistant/components/imap/errors.py new file mode 100644 index 00000000000000..8f91b7ab6df4d8 --- /dev/null +++ b/homeassistant/components/imap/errors.py @@ -0,0 +1,11 @@ +"""Exceptions raised by IMAP integration.""" + +from homeassistant.exceptions import HomeAssistantError + + +class InvalidAuth(HomeAssistantError): + """Raise exception for invalid credentials.""" + + +class InvalidFolder(HomeAssistantError): + """Raise exception for invalid folder.""" diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 36004113351570..24a9486107a085 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -1,9 +1,11 @@ { "domain": "imap", "name": "IMAP", + "config_flow": true, + "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": ["aioimaplib==1.0.1"], - "codeowners": [], + "codeowners": ["@engrbm87"], "iot_class": "cloud_push", "loggers": ["aioimaplib"] } diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index fa5428ccc06df8..20457209e994ce 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -1,37 +1,29 @@ """IMAP sensor support.""" from __future__ import annotations -import asyncio -import logging - -from aioimaplib import IMAP4_SSL, AioImapException -import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -_LOGGER = logging.getLogger(__name__) - -CONF_SERVER = "server" -CONF_FOLDER = "folder" -CONF_SEARCH = "search" -CONF_CHARSET = "charset" - -DEFAULT_PORT = 993 - -ICON = "mdi:email-outline" +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import ImapDataUpdateCoordinator +from .const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + CONF_SERVER, + DEFAULT_PORT, + DOMAIN, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -54,139 +46,60 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the IMAP platform.""" - sensor = ImapSensor( - config.get(CONF_NAME), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - config.get(CONF_SERVER), - config.get(CONF_PORT), - config.get(CONF_CHARSET), - config.get(CONF_FOLDER), - config.get(CONF_SEARCH), + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.4.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) ) - if not await sensor.connection(): - raise PlatformNotReady - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.shutdown) - async_add_entities([sensor], True) +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Imap sensor.""" -class ImapSensor(SensorEntity): - """Representation of an IMAP sensor.""" + coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - def __init__(self, name, user, password, server, port, charset, folder, search): - """Initialize the sensor.""" - self._name = name or user - self._user = user - self._password = password - self._server = server - self._port = port - self._charset = charset - self._folder = folder - self._email_count = None - self._search = search - self._connection = None - self._does_push = None - self._idle_loop_task = None - - async def async_added_to_hass(self) -> None: - """Handle when an entity is about to be added to Home Assistant.""" - if not self.should_poll: - self._idle_loop_task = self.hass.loop.create_task(self.idle_loop()) + async_add_entities([ImapSensor(coordinator)]) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON +class ImapSensor(CoordinatorEntity[ImapDataUpdateCoordinator], SensorEntity): + """Representation of an IMAP sensor.""" - @property - def native_value(self): - """Return the number of emails found.""" - return self._email_count + _attr_icon = "mdi:email-outline" + _attr_has_entity_name = True - @property - def available(self) -> bool: - """Return the availability of the device.""" - return self._connection is not None + def __init__(self, coordinator: ImapDataUpdateCoordinator) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + # To be removed when YAML import is removed + if CONF_NAME in coordinator.config_entry.data: + self._attr_name = coordinator.config_entry.data[CONF_NAME] + self._attr_has_entity_name = False + self._attr_unique_id = f"{coordinator.config_entry.entry_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=f"IMAP ({coordinator.config_entry.data[CONF_USERNAME]})", + entry_type=DeviceEntryType.SERVICE, + ) @property - def should_poll(self) -> bool: - """Return if polling is needed.""" - return not self._does_push - - async def connection(self): - """Return a connection to the server, establishing it if necessary.""" - if self._connection is None: - try: - self._connection = IMAP4_SSL(self._server, self._port) - await self._connection.wait_hello_from_server() - await self._connection.login(self._user, self._password) - await self._connection.select(self._folder) - self._does_push = self._connection.has_capability("IDLE") - except (AioImapException, asyncio.TimeoutError): - self._connection = None - - return self._connection - - async def idle_loop(self): - """Wait for data pushed from server.""" - while True: - try: - if await self.connection(): - await self.refresh_email_count() - self.async_write_ha_state() - - idle = await self._connection.idle_start() - await self._connection.wait_server_push() - self._connection.idle_done() - async with async_timeout.timeout(10): - await idle - else: - self.async_write_ha_state() - except (AioImapException, asyncio.TimeoutError): - self.disconnected() + def native_value(self) -> int: + """Return the number of emails found.""" + return self.coordinator.data async def async_update(self) -> None: - """Periodic polling of state.""" - try: - if await self.connection(): - await self.refresh_email_count() - except (AioImapException, asyncio.TimeoutError): - self.disconnected() - - async def refresh_email_count(self): - """Check the number of found emails.""" - if self._connection: - await self._connection.noop() - result, lines = await self._connection.search( - self._search, charset=self._charset - ) - - if result == "OK": - self._email_count = len(lines[0].split()) - else: - _LOGGER.error( - "Can't parse IMAP server response to search '%s': %s / %s", - self._search, - result, - lines[0], - ) - - def disconnected(self): - """Forget the connection after it was lost.""" - _LOGGER.warning("Lost %s (will attempt to reconnect)", self._server) - self._connection = None - - async def shutdown(self, *_): - """Close resources.""" - if self._connection: - if self._connection.has_pending_idle(): - self._connection.idle_done() - await self._connection.logout() - if self._idle_loop_task: - self._idle_loop_task.cancel() + """Check for idle state before updating.""" + if not await self.coordinator.imap_client.stop_wait_server_push(): + await super().async_update() diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json new file mode 100644 index 00000000000000..25bcf840c33472 --- /dev/null +++ b/homeassistant/components/imap/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "server": "Server", + "port": "[%key:common::config_flow::data::port%]", + "charset": "Character set", + "folder": "Folder", + "search": "IMAP search" + } + }, + "reauth_confirm": { + "description": "The password for {username} is invalid.", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_charset": "The specified charset is not supported", + "invalid_search": "The selected search is invalid" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The IMAP YAML configuration is being removed", + "description": "Configuring IMAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the IMAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/imap/translations/en.json b/homeassistant/components/imap/translations/en.json new file mode 100644 index 00000000000000..a1317b32f196ea --- /dev/null +++ b/homeassistant/components/imap/translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_charset": "The specified charset is not supported", + "invalid_search": "The selected search is invalid" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is invalid.", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "charset": "Character set", + "folder": "Folder", + "password": "Password", + "port": "Port", + "search": "IMAP search", + "server": "Server", + "username": "Username" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring IMAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the IMAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The IMAP YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 58100b9c2beeb6..674deeb46ba01a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -191,6 +191,7 @@ "ibeacon", "icloud", "ifttt", + "imap", "inkbird", "insteon", "intellifire", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4333cb51ff52fc..4507aa969a3869 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2439,7 +2439,7 @@ "imap": { "name": "IMAP", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_push" }, "imap_email_content": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eee3ae9af5a0b..18976c2731737c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -170,6 +170,9 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==4.5.0 +# homeassistant.components.imap +aioimaplib==1.0.1 + # homeassistant.components.apache_kafka aiokafka==0.7.2 diff --git a/tests/components/imap/__init__.py b/tests/components/imap/__init__.py new file mode 100644 index 00000000000000..db4c252334cdbd --- /dev/null +++ b/tests/components/imap/__init__.py @@ -0,0 +1 @@ +"""Tests for the imap integration.""" diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py new file mode 100644 index 00000000000000..7fc5f998843e0c --- /dev/null +++ b/tests/components/imap/test_config_flow.py @@ -0,0 +1,349 @@ +"""Test the imap config flow.""" +import asyncio +from unittest.mock import patch + +from aioimaplib import AioImapException +import pytest + +from homeassistant import config_entries +from homeassistant.components.imap.const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + DOMAIN, +) +from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", +} + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "email@email.com" + assert result2["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "name": "IMAP", + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IMAP" + assert result2["data"] == { + "name": "IMAP", + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_entry_already_configured(hass: HomeAssistant) -> None: + """Test aborting if the entry is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == { + CONF_USERNAME: "invalid_auth", + CONF_PASSWORD: "invalid_auth", + } + + +@pytest.mark.parametrize( + "exc", + [asyncio.TimeoutError, AioImapException("")], +) +async def test_form_cannot_connect(hass: HomeAssistant, exc: Exception) -> None: + """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.imap.config_flow.connect_to_server", + side_effect=exc, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_charset(hass: HomeAssistant) -> None: + """Test we handle invalid charset.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "NO", + [b"The specified charset is not supported"], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_CHARSET: "invalid_charset"} + + +async def test_form_invalid_folder(hass: HomeAssistant) -> None: + """Test we handle invalid folder selection.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidFolder, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_FOLDER: "invalid_folder"} + + +async def test_form_invalid_search(hass: HomeAssistant) -> None: + """Test we handle invalid search.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "BAD", + [b"Invalid search"], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_SEARCH: "invalid_search"} + + +async def test_reauth_success(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {CONF_USERNAME: "email@email.com"} + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_failed(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == { + CONF_USERNAME: "invalid_auth", + CONF_PASSWORD: "invalid_auth", + } + + +async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"}