From cf0f79294bef3c138ecdf1d12a2c86e028399c6f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 26 Oct 2022 21:50:53 +0200 Subject: [PATCH 001/394] Bumped version to 2022.11.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 112b1637c4684a..c194782ed29819 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 3ca463d6fc1a05..a869da99baa752 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0.dev0" +version = "2022.11.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 11cc7e156626991353ff78efd21378626c570ecb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 27 Oct 2022 21:51:09 +0200 Subject: [PATCH 002/394] Add WS API recorder/statistic_during_period (#80663) --- .../components/recorder/statistics.py | 373 ++++++++++++- .../components/recorder/websocket_api.py | 148 +++++- .../components/recorder/test_websocket_api.py | 491 +++++++++++++++++- 3 files changed, 987 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2b249aeeb14cbf..8a744fd4daabd8 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1113,6 +1113,377 @@ def _statistics_during_period_stmt_short_term( return stmt +def _get_max_mean_min_statistic_in_sub_period( + session: Session, + result: dict[str, float], + start_time: datetime | None, + end_time: datetime | None, + table: type[Statistics | StatisticsShortTerm], + types: set[str], + metadata_id: int, +) -> None: + """Return max, mean and min during the period.""" + # Calculate max, mean, min + columns = [] + if "max" in types: + columns.append(func.max(table.max)) + if "mean" in types: + columns.append(func.avg(table.mean)) + columns.append(func.count(table.mean)) + if "min" in types: + columns.append(func.min(table.min)) + stmt = lambda_stmt(lambda: select(columns).filter(table.metadata_id == metadata_id)) + if start_time is not None: + stmt += lambda q: q.filter(table.start >= start_time) + if end_time is not None: + stmt += lambda q: q.filter(table.start < end_time) + stats = execute_stmt_lambda_element(session, stmt) + if "max" in types and stats and (new_max := stats[0].max) is not None: + old_max = result.get("max") + result["max"] = max(new_max, old_max) if old_max is not None else new_max + if "mean" in types and stats and stats[0].avg is not None: + duration = stats[0].count * table.duration.total_seconds() + result["duration"] = result.get("duration", 0.0) + duration + result["mean_acc"] = result.get("mean_acc", 0.0) + stats[0].avg * duration + if "min" in types and stats and (new_min := stats[0].min) is not None: + old_min = result.get("min") + result["min"] = min(new_min, old_min) if old_min is not None else new_min + + +def _get_max_mean_min_statistic( + session: Session, + head_start_time: datetime | None, + head_end_time: datetime | None, + main_start_time: datetime | None, + main_end_time: datetime | None, + tail_start_time: datetime | None, + tail_end_time: datetime | None, + tail_only: bool, + metadata_id: int, + types: set[str], +) -> dict[str, float | None]: + """Return max, mean and min during the period. + + The mean is a time weighted average, combining hourly and 5-minute statistics if + necessary. + """ + max_mean_min: dict[str, float] = {} + result: dict[str, float | None] = {} + + if tail_start_time is not None: + # Calculate max, mean, min + _get_max_mean_min_statistic_in_sub_period( + session, + max_mean_min, + tail_start_time, + tail_end_time, + StatisticsShortTerm, + types, + metadata_id, + ) + + if not tail_only: + _get_max_mean_min_statistic_in_sub_period( + session, + max_mean_min, + main_start_time, + main_end_time, + Statistics, + types, + metadata_id, + ) + + if head_start_time is not None: + _get_max_mean_min_statistic_in_sub_period( + session, + max_mean_min, + head_start_time, + head_end_time, + StatisticsShortTerm, + types, + metadata_id, + ) + + if "max" in types: + result["max"] = max_mean_min.get("max") + if "mean" in types: + if "mean_acc" not in max_mean_min: + result["mean"] = None + else: + result["mean"] = max_mean_min["mean_acc"] / max_mean_min["duration"] + if "min" in types: + result["min"] = max_mean_min.get("min") + return result + + +def _get_oldest_sum_statistic( + session: Session, + head_start_time: datetime | None, + main_start_time: datetime | None, + tail_start_time: datetime | None, + tail_only: bool, + metadata_id: int, +) -> float | None: + """Return the oldest non-NULL sum during the period.""" + + def _get_oldest_sum_statistic_in_sub_period( + session: Session, + start_time: datetime | None, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + ) -> tuple[float | None, datetime | None]: + """Return the oldest non-NULL sum during the period.""" + stmt = lambda_stmt( + lambda: select(table.sum, table.start) + .filter(table.metadata_id == metadata_id) + .filter(table.sum.is_not(None)) + .order_by(table.start.asc()) + .limit(1) + ) + if start_time is not None: + start_time = start_time + table.duration - timedelta.resolution + if table == StatisticsShortTerm: + minutes = start_time.minute - start_time.minute % 5 + period = start_time.replace(minute=minutes, second=0, microsecond=0) + else: + period = start_time.replace(minute=0, second=0, microsecond=0) + prev_period = period - table.duration + stmt += lambda q: q.filter(table.start == prev_period) + stats = execute_stmt_lambda_element(session, stmt) + return ( + (stats[0].sum, process_timestamp(stats[0].start)) if stats else (None, None) + ) + + oldest_start: datetime | None + oldest_sum: float | None = None + + if head_start_time is not None: + oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( + session, head_start_time, StatisticsShortTerm, metadata_id + ) + if ( + oldest_start is not None + and oldest_start < head_start_time + and oldest_sum is not None + ): + return oldest_sum + + if not tail_only: + assert main_start_time is not None + oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( + session, main_start_time, Statistics, metadata_id + ) + if ( + oldest_start is not None + and oldest_start < main_start_time + and oldest_sum is not None + ): + return oldest_sum + return 0 + + if tail_start_time is not None: + oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period( + session, tail_start_time, StatisticsShortTerm, metadata_id + ) + if ( + oldest_start is not None + and oldest_start < tail_start_time + and oldest_sum is not None + ): + return oldest_sum + + return 0 + + +def _get_newest_sum_statistic( + session: Session, + head_start_time: datetime | None, + head_end_time: datetime | None, + main_start_time: datetime | None, + main_end_time: datetime | None, + tail_start_time: datetime | None, + tail_end_time: datetime | None, + tail_only: bool, + metadata_id: int, +) -> float | None: + """Return the newest non-NULL sum during the period.""" + + def _get_newest_sum_statistic_in_sub_period( + session: Session, + start_time: datetime | None, + end_time: datetime | None, + table: type[Statistics | StatisticsShortTerm], + metadata_id: int, + ) -> float | None: + """Return the newest non-NULL sum during the period.""" + stmt = lambda_stmt( + lambda: select( + table.sum, + ) + .filter(table.metadata_id == metadata_id) + .filter(table.sum.is_not(None)) + .order_by(table.start.desc()) + .limit(1) + ) + if start_time is not None: + stmt += lambda q: q.filter(table.start >= start_time) + if end_time is not None: + stmt += lambda q: q.filter(table.start < end_time) + stats = execute_stmt_lambda_element(session, stmt) + + return stats[0].sum if stats else None + + newest_sum: float | None = None + + if tail_start_time is not None: + newest_sum = _get_newest_sum_statistic_in_sub_period( + session, tail_start_time, tail_end_time, StatisticsShortTerm, metadata_id + ) + if newest_sum is not None: + return newest_sum + + if not tail_only: + newest_sum = _get_newest_sum_statistic_in_sub_period( + session, main_start_time, main_end_time, Statistics, metadata_id + ) + if newest_sum is not None: + return newest_sum + + if head_start_time is not None: + newest_sum = _get_newest_sum_statistic_in_sub_period( + session, head_start_time, head_end_time, StatisticsShortTerm, metadata_id + ) + + return newest_sum + + +def statistic_during_period( + hass: HomeAssistant, + start_time: datetime | None, + end_time: datetime | None, + statistic_id: str, + types: set[str] | None, + units: dict[str, str] | None, +) -> dict[str, Any]: + """Return a statistic data point for the UTC period start_time - end_time.""" + metadata = None + + if not types: + types = {"max", "mean", "min", "change"} + + result: dict[str, Any] = {} + + # To calculate the summary, data from the statistics (hourly) and short_term_statistics + # (5 minute) tables is combined + # - The short term statistics table is used for the head and tail of the period, + # if the period it doesn't start or end on a full hour + # - The statistics table is used for the remainder of the time + now = dt_util.utcnow() + if end_time is not None and end_time > now: + end_time = now + + tail_only = ( + start_time is not None + and end_time is not None + and end_time - start_time < timedelta(hours=1) + ) + + # Calculate the head period + head_start_time: datetime | None = None + head_end_time: datetime | None = None + if not tail_only and start_time is not None and start_time.minute: + head_start_time = start_time + head_end_time = start_time.replace( + minute=0, second=0, microsecond=0 + ) + timedelta(hours=1) + + # Calculate the tail period + tail_start_time: datetime | None = None + tail_end_time: datetime | None = None + if end_time is None: + tail_start_time = now.replace(minute=0, second=0, microsecond=0) + elif end_time.minute: + tail_start_time = ( + start_time + if tail_only + else end_time.replace(minute=0, second=0, microsecond=0) + ) + tail_end_time = end_time + + # Calculate the main period + main_start_time: datetime | None = None + main_end_time: datetime | None = None + if not tail_only: + main_start_time = start_time if head_end_time is None else head_end_time + main_end_time = end_time if tail_start_time is None else tail_start_time + + with session_scope(hass=hass) as session: + # Fetch metadata for the given statistic_id + metadata = get_metadata_with_session(session, statistic_ids=[statistic_id]) + if not metadata: + return result + + metadata_id = metadata[statistic_id][0] + + if not types.isdisjoint({"max", "mean", "min"}): + result = _get_max_mean_min_statistic( + session, + head_start_time, + head_end_time, + main_start_time, + main_end_time, + tail_start_time, + tail_end_time, + tail_only, + metadata_id, + types, + ) + + if "change" in types: + oldest_sum: float | None + if start_time is None: + oldest_sum = 0.0 + else: + oldest_sum = _get_oldest_sum_statistic( + session, + head_start_time, + main_start_time, + tail_start_time, + tail_only, + metadata_id, + ) + newest_sum = _get_newest_sum_statistic( + session, + head_start_time, + head_end_time, + main_start_time, + main_end_time, + tail_start_time, + tail_end_time, + tail_only, + metadata_id, + ) + # Calculate the difference between the oldest and newest sum + if oldest_sum is not None and newest_sum is not None: + result["change"] = newest_sum - oldest_sum + else: + result["change"] = None + + def no_conversion(val: float | None) -> float | None: + """Return val.""" + return val + + state_unit = unit = metadata[statistic_id][1]["unit_of_measurement"] + if state := hass.states.get(statistic_id): + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit is not None: + convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) + else: + convert = no_conversion + + return {key: convert(value) for key, value in result.items()} + + def statistics_during_period( hass: HomeAssistant, start_time: datetime, @@ -1122,7 +1493,7 @@ def statistics_during_period( start_time_as_datetime: bool = False, units: dict[str, str] | None = None, ) -> dict[str, list[dict[str, Any]]]: - """Return statistics during UTC period start_time - end_time for the statistic_ids. + """Return statistic data points during UTC period start_time - end_time. If end_time is omitted, returns statistics newer than or equal to start_time. If statistic_ids is omitted, returns statistics for all statistics ids. diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 2079d9537b5588..9b2ef417755c40 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,7 +1,7 @@ """The Recorder websocket API.""" from __future__ import annotations -from datetime import datetime as dt +from datetime import datetime as dt, timedelta import logging from typing import Any, Literal @@ -10,6 +10,7 @@ from homeassistant.components import websocket_api from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant, callback, valid_entity_id +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util @@ -31,6 +32,7 @@ async_change_statistics_unit, async_import_statistics, list_statistic_ids, + statistic_during_period, statistics_during_period, validate_statistics, ) @@ -47,6 +49,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_backup_start) websocket_api.async_register_command(hass, ws_change_statistics_unit) websocket_api.async_register_command(hass, ws_clear_statistics) + websocket_api.async_register_command(hass, ws_get_statistic_during_period) websocket_api.async_register_command(hass, ws_get_statistics_during_period) websocket_api.async_register_command(hass, ws_get_statistics_metadata) websocket_api.async_register_command(hass, ws_list_statistic_ids) @@ -56,6 +59,149 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_validate_statistics) +def _ws_get_statistic_during_period( + hass: HomeAssistant, + msg_id: int, + start_time: dt | None, + end_time: dt | None, + statistic_id: str, + types: set[str] | None, + units: dict[str, str], +) -> str: + """Fetch statistics and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + statistic_during_period( + hass, start_time, end_time, statistic_id, types, units=units + ), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/statistic_during_period", + vol.Exclusive("calendar", "period"): vol.Schema( + { + vol.Required("period"): vol.Any("hour", "day", "week", "month", "year"), + vol.Optional("offset"): int, + } + ), + vol.Exclusive("fixed_period", "period"): vol.Schema( + { + vol.Optional("start_time"): str, + vol.Optional("end_time"): str, + } + ), + vol.Exclusive("rolling_window", "period"): vol.Schema( + { + vol.Required("duration"): cv.time_period_dict, + vol.Optional("offset"): cv.time_period_dict, + } + ), + vol.Optional("statistic_id"): str, + vol.Optional("types"): vol.All([str], vol.Coerce(set)), + vol.Optional("units"): vol.Schema( + { + vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), + vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), + vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), + vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), + vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), + vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), + vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), + } + ), + } +) +@websocket_api.async_response +async def ws_get_statistic_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle statistics websocket command.""" + if ("start_time" in msg or "end_time" in msg) and "duration" in msg: + raise HomeAssistantError + if "offset" in msg and "duration" not in msg: + raise HomeAssistantError + + start_time = None + end_time = None + + if "calendar" in msg: + calendar_period = msg["calendar"]["period"] + start_of_day = dt_util.start_of_local_day() + offset = msg["calendar"].get("offset", 0) + if calendar_period == "hour": + start_time = dt_util.now().replace(minute=0, second=0, microsecond=0) + start_time += timedelta(hours=offset) + end_time = start_time + timedelta(hours=1) + elif calendar_period == "day": + start_time = start_of_day + start_time += timedelta(days=offset) + end_time = start_time + timedelta(days=1) + elif calendar_period == "week": + start_time = start_of_day - timedelta(days=start_of_day.weekday()) + start_time += timedelta(days=offset * 7) + end_time = start_time + timedelta(weeks=1) + elif calendar_period == "month": + start_time = start_of_day.replace(day=28) + # This works for up to 48 months of offset + start_time = (start_time + timedelta(days=offset * 31)).replace(day=1) + end_time = (start_time + timedelta(days=31)).replace(day=1) + else: # calendar_period = "year" + start_time = start_of_day.replace(month=12, day=31) + # This works for 100+ years of offset + start_time = (start_time + timedelta(days=offset * 366)).replace( + month=1, day=1 + ) + end_time = (start_time + timedelta(days=365)).replace(day=1) + + start_time = dt_util.as_utc(start_time) + end_time = dt_util.as_utc(end_time) + + elif "fixed_period" in msg: + if start_time_str := msg["fixed_period"].get("start_time"): + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error( + msg["id"], "invalid_start_time", "Invalid start_time" + ) + return + + if end_time_str := msg["fixed_period"].get("end_time"): + if end_time := dt_util.parse_datetime(end_time_str): + end_time = dt_util.as_utc(end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + + elif "rolling_window" in msg: + duration = msg["rolling_window"]["duration"] + now = dt_util.utcnow() + start_time = now - duration + end_time = start_time + duration + + if offset := msg["rolling_window"].get("offset"): + start_time += offset + end_time += offset + + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_statistic_during_period, + hass, + msg["id"], + start_time, + end_time, + msg.get("statistic_id"), + msg.get("types"), + msg.get("units"), + ) + ) + + def _ws_get_statistics_during_period( hass: HomeAssistant, msg_id: int, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 58cf0e5c6634e7..00e9d0d35b4e6b 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1,14 +1,17 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name +import datetime from datetime import timedelta +from statistics import fmean import threading -from unittest.mock import patch +from unittest.mock import ANY, patch from freezegun import freeze_time import pytest from pytest import approx from homeassistant.components import recorder +from homeassistant.components.recorder.db_schema import Statistics, StatisticsShortTerm from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, @@ -178,6 +181,448 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client): } +@freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) +async def test_statistic_during_period(recorder_mock, hass, hass_ws_client): + """Test statistic_during_period.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + now = dt_util.utcnow() + + await async_recorder_block_till_done(hass) + client = await hass_ws_client() + + zero = now + start = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=-3) + + imported_stats_5min = [ + { + "start": (start + timedelta(minutes=5 * i)), + "max": i * 2, + "mean": i, + "min": -76 + i * 2, + "sum": i, + } + for i in range(0, 39) + ] + imported_stats = [ + { + "start": imported_stats_5min[i * 12]["start"], + "max": max( + stat["max"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] + ), + "mean": fmean( + stat["mean"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] + ), + "min": min( + stat["min"] for stat in imported_stats_5min[i * 12 : (i + 1) * 12] + ), + "sum": imported_stats_5min[i * 12 + 11]["sum"], + } + for i in range(0, 3) + ] + imported_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.test", + "unit_of_measurement": "kWh", + } + + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats, + Statistics, + ) + recorder.get_instance(hass).async_import_statistics( + imported_metadata, + imported_stats_5min, + StatisticsShortTerm, + ) + await async_wait_recording_done(hass) + + # No data for this period yet + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": now.isoformat(), + "end_time": now.isoformat(), + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": None, + "mean": None, + "min": None, + "change": None, + } + + # This should include imported_statistics_5min[:] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]), + "min": min(stat["min"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # This should also include imported_statistics_5min[:] + start_time = "2022-10-21T04:00:00+00:00" + end_time = "2022-10-21T07:15:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]), + "min": min(stat["min"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # This should also include imported_statistics_5min[:] + start_time = "2022-10-20T04:00:00+00:00" + end_time = "2022-10-21T08:20:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]), + "min": min(stat["min"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # This should include imported_statistics_5min[26:] + start_time = "2022-10-21T06:10:00+00:00" + assert imported_stats_5min[26]["start"].isoformat() == start_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[26:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[26:]), + "min": min(stat["min"] for stat in imported_stats_5min[26:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[25]["sum"], + } + + # This should also include imported_statistics_5min[26:] + start_time = "2022-10-21T06:09:00+00:00" + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[26:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[26:]), + "min": min(stat["min"] for stat in imported_stats_5min[26:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[25]["sum"], + } + + # This should include imported_statistics_5min[:26] + end_time = "2022-10-21T06:10:00+00:00" + assert imported_stats_5min[26]["start"].isoformat() == end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "end_time": end_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:26]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:26]), + "min": min(stat["min"] for stat in imported_stats_5min[:26]), + "change": imported_stats_5min[25]["sum"] - 0, + } + + # This should include imported_statistics_5min[26:32] (less than a full hour) + start_time = "2022-10-21T06:10:00+00:00" + assert imported_stats_5min[26]["start"].isoformat() == start_time + end_time = "2022-10-21T06:40:00+00:00" + assert imported_stats_5min[32]["start"].isoformat() == end_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + "end_time": end_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[26:32]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[26:32]), + "min": min(stat["min"] for stat in imported_stats_5min[26:32]), + "change": imported_stats_5min[31]["sum"] - imported_stats_5min[25]["sum"], + } + + # This should include imported_statistics[2:] + imported_statistics_5min[36:] + start_time = "2022-10-21T06:00:00+00:00" + assert imported_stats_5min[24]["start"].isoformat() == start_time + assert imported_stats[2]["start"].isoformat() == start_time + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "fixed_period": { + "start_time": start_time, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[24:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), + "min": min(stat["min"] for stat in imported_stats_5min[24:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + } + + # This should also include imported_statistics[2:] + imported_statistics_5min[36:] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "rolling_window": { + "duration": {"hours": 1, "minutes": 25}, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[24:]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:]), + "min": min(stat["min"] for stat in imported_stats_5min[24:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[23]["sum"], + } + + # This should include imported_statistics[2:3] + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "rolling_window": { + "duration": {"hours": 1}, + "offset": {"minutes": -25}, + }, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[24:36]), + "mean": fmean(stat["mean"] for stat in imported_stats_5min[24:36]), + "min": min(stat["min"] for stat in imported_stats_5min[24:36]), + "change": imported_stats_5min[35]["sum"] - imported_stats_5min[23]["sum"], + } + + # Test we can get only selected types + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "types": ["max", "change"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]), + "change": imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"], + } + + # Test we can convert units + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + "units": {"energy": "MWh"}, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]) / 1000, + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]) / 1000, + "min": min(stat["min"] for stat in imported_stats_5min[:]) / 1000, + "change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"]) + / 1000, + } + + # Test we can automatically convert units + hass.states.async_set("sensor.test", None, attributes=ENERGY_SENSOR_WH_ATTRIBUTES) + await client.send_json( + { + "id": next_id(), + "type": "recorder/statistic_during_period", + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "max": max(stat["max"] for stat in imported_stats_5min[:]) * 1000, + "mean": fmean(stat["mean"] for stat in imported_stats_5min[:]) * 1000, + "min": min(stat["min"] for stat in imported_stats_5min[:]) * 1000, + "change": (imported_stats_5min[-1]["sum"] - imported_stats_5min[0]["sum"]) + * 1000, + } + + +@freeze_time(datetime.datetime(2022, 10, 21, 7, 25, tzinfo=datetime.timezone.utc)) +@pytest.mark.parametrize( + "calendar_period, start_time, end_time", + ( + ( + {"period": "hour"}, + "2022-10-21T07:00:00+00:00", + "2022-10-21T08:00:00+00:00", + ), + ( + {"period": "hour", "offset": -1}, + "2022-10-21T06:00:00+00:00", + "2022-10-21T07:00:00+00:00", + ), + ( + {"period": "day"}, + "2022-10-21T07:00:00+00:00", + "2022-10-22T07:00:00+00:00", + ), + ( + {"period": "day", "offset": -1}, + "2022-10-20T07:00:00+00:00", + "2022-10-21T07:00:00+00:00", + ), + ( + {"period": "week"}, + "2022-10-17T07:00:00+00:00", + "2022-10-24T07:00:00+00:00", + ), + ( + {"period": "week", "offset": -1}, + "2022-10-10T07:00:00+00:00", + "2022-10-17T07:00:00+00:00", + ), + ( + {"period": "month"}, + "2022-10-01T07:00:00+00:00", + "2022-11-01T07:00:00+00:00", + ), + ( + {"period": "month", "offset": -1}, + "2022-09-01T07:00:00+00:00", + "2022-10-01T07:00:00+00:00", + ), + ( + {"period": "year"}, + "2022-01-01T08:00:00+00:00", + "2023-01-01T08:00:00+00:00", + ), + ( + {"period": "year", "offset": -1}, + "2021-01-01T08:00:00+00:00", + "2022-01-01T08:00:00+00:00", + ), + ), +) +async def test_statistic_during_period_calendar( + recorder_mock, hass, hass_ws_client, calendar_period, start_time, end_time +): + """Test statistic_during_period.""" + client = await hass_ws_client() + + # Try requesting data for the current hour + with patch( + "homeassistant.components.recorder.websocket_api.statistic_during_period", + return_value={}, + ) as statistic_during_period: + await client.send_json( + { + "id": 1, + "type": "recorder/statistic_during_period", + "calendar": calendar_period, + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + statistic_during_period.assert_called_once_with( + hass, ANY, ANY, "sensor.test", None, units=None + ) + assert statistic_during_period.call_args[0][1].isoformat() == start_time + assert statistic_during_period.call_args[0][2].isoformat() == end_time + assert response["success"] + + @pytest.mark.parametrize( "attributes, state, value, custom_units, converted_value", [ @@ -1595,20 +2040,20 @@ async def test_import_statistics( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -1621,8 +2066,8 @@ async def test_import_statistics( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() @@ -1712,7 +2157,7 @@ async def test_import_statistics( { "id": 2, "type": "recorder/import_statistics", - "metadata": external_metadata, + "metadata": imported_metadata, "stats": [external_statistics], } ) @@ -1764,7 +2209,7 @@ async def test_import_statistics( { "id": 3, "type": "recorder/import_statistics", - "metadata": external_metadata, + "metadata": imported_metadata, "stats": [external_statistics], } ) @@ -1822,20 +2267,20 @@ async def test_adjust_sum_statistics_energy( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -1848,8 +2293,8 @@ async def test_adjust_sum_statistics_energy( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() @@ -2018,20 +2463,20 @@ async def test_adjust_sum_statistics_gas( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -2044,8 +2489,8 @@ async def test_adjust_sum_statistics_gas( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() @@ -2229,20 +2674,20 @@ async def test_adjust_sum_statistics_errors( period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) - external_statistics1 = { + imported_statistics1 = { "start": period1.isoformat(), "last_reset": None, "state": 0, "sum": 2, } - external_statistics2 = { + imported_statistics2 = { "start": period2.isoformat(), "last_reset": None, "state": 1, "sum": 3, } - external_metadata = { + imported_metadata = { "has_mean": False, "has_sum": True, "name": "Total imported energy", @@ -2255,8 +2700,8 @@ async def test_adjust_sum_statistics_errors( { "id": 1, "type": "recorder/import_statistics", - "metadata": external_metadata, - "stats": [external_statistics1, external_statistics2], + "metadata": imported_metadata, + "stats": [imported_statistics1, imported_statistics2], } ) response = await client.receive_json() From b5615823bababfc17fa6b92ef95ce173f502e602 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Oct 2022 17:05:09 -0500 Subject: [PATCH 003/394] Bump aiohomekit to 2.2.5 (#81048) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 0bad4ed9f7bd18..24b2eebe61575b 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.4"], + "requirements": ["aiohomekit==2.2.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4a6b3708aaec47..709b686cbf1524 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.4 +aiohomekit==2.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b39523ca427a6..7992fe48b6ef98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.4 +aiohomekit==2.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http From ad29bd55a45e7d04bc5f6f02f912ed0ccd7252c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Oct 2022 18:03:13 -0500 Subject: [PATCH 004/394] Bump zeroconf to 0.39.3 (#81049) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index f0e2005b20edbb..967dd761ac7977 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.39.2"], + "requirements": ["zeroconf==0.39.3"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c7dd51d76dff9a..0a4ccf0f58e888 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.8.1 -zeroconf==0.39.2 +zeroconf==0.39.3 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 709b686cbf1524..a5415a0e50f103 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2604,7 +2604,7 @@ zamg==0.1.1 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.2 +zeroconf==0.39.3 # homeassistant.components.zha zha-quirks==0.0.84 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7992fe48b6ef98..522a69b3b1c908 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1805,7 +1805,7 @@ youless-api==0.16 zamg==0.1.1 # homeassistant.components.zeroconf -zeroconf==0.39.2 +zeroconf==0.39.3 # homeassistant.components.zha zha-quirks==0.0.84 From 200f0fa92c876e2d92ecb433842c2da2d7fcb902 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 26 Oct 2022 21:47:38 -0400 Subject: [PATCH 005/394] Bump zigpy to 0.51.4 (#81050) Bump zigpy from 0.51.3 to 0.51.4 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 4698c78d384da0..1c86fe52c5e059 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.84", "zigpy-deconz==0.19.0", - "zigpy==0.51.3", + "zigpy==0.51.4", "zigpy-xbee==0.16.2", "zigpy-zigate==0.10.2", "zigpy-znp==0.9.1" diff --git a/requirements_all.txt b/requirements_all.txt index a5415a0e50f103..387e9e33360704 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2628,7 +2628,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.3 +zigpy==0.51.4 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 522a69b3b1c908..fd785aa01c3ef4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1823,7 +1823,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.3 +zigpy==0.51.4 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From bb47935509dc492d55f7b1c09ef4588ca930d363 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 26 Oct 2022 21:29:48 -0400 Subject: [PATCH 006/394] Handle sending ZCL commands with empty bitmap options (#81051) Handle sending commands with empty bitmaps --- homeassistant/components/zha/core/helpers.py | 30 +++++++------------- tests/components/zha/test_helpers.py | 14 +++++++++ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 409d45789b56b1..1ea9a2a4c9be2b 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -14,7 +14,6 @@ import functools import itertools import logging -import operator from random import uniform import re from typing import TYPE_CHECKING, Any, TypeVar @@ -163,25 +162,16 @@ def convert_to_zcl_values( if field.name not in fields: continue value = fields[field.name] - if issubclass(field.type, enum.Flag): - if isinstance(value, list): - value = field.type( - functools.reduce( - operator.ior, - [ - field.type[flag.replace(" ", "_")] - if isinstance(flag, str) - else field.type(flag) - for flag in value - ], - ) - ) - else: - value = ( - field.type[value.replace(" ", "_")] - if isinstance(value, str) - else field.type(value) - ) + if issubclass(field.type, enum.Flag) and isinstance(value, list): + new_value = 0 + + for flag in value: + if isinstance(flag, str): + new_value |= field.type[flag.replace(" ", "_")] + else: + new_value |= flag + + value = field.type(new_value) elif issubclass(field.type, enum.Enum): value = ( field.type[value.replace(" ", "_")] diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index f5fb5c4f5c090f..64f8c732ca98ef 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -195,3 +195,17 @@ async def test_zcl_schema_conversions(hass, device_light): assert isinstance(converted_data["start_hue"], uint16_t) assert converted_data["start_hue"] == 196 + + # This time, the update flags bitmap is empty + raw_data = { + "update_flags": [], + "action": 0x02, + "direction": 0x01, + "time": 20, + "start_hue": 196, + } + + converted_data = convert_to_zcl_values(raw_data, command_schema) + + # No flags are passed through + assert converted_data["update_flags"] == 0 From c10dd1b7028a7a1dde9d1cf426c917f991ab32c5 Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Thu, 27 Oct 2022 01:37:48 -0400 Subject: [PATCH 007/394] Eight Sleep catch missing keys (#81058) Catch missing keys --- homeassistant/components/eight_sleep/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index b184cd2496f951..b07865d8591e09 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -146,7 +146,7 @@ def _get_breakdown_percent( """Get a breakdown percent.""" try: return round((attr["breakdown"][key] / denominator) * 100, 2) - except ZeroDivisionError: + except (ZeroDivisionError, KeyError): return 0 From 61d064ffd567b944fc2629e8c3e4d5d14304c1a7 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Thu, 27 Oct 2022 15:01:15 +1100 Subject: [PATCH 008/394] Bump aiolifx-themes to 0.2.0 (#81059) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index da07a2ffc8b2f5..fc5422757b99e2 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "aiolifx==0.8.6", "aiolifx_effects==0.3.0", - "aiolifx_themes==0.1.1" + "aiolifx_themes==0.2.0" ], "quality_scale": "platinum", "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 387e9e33360704..d08127cc55805f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ aiolifx==0.8.6 aiolifx_effects==0.3.0 # homeassistant.components.lifx -aiolifx_themes==0.1.1 +aiolifx_themes==0.2.0 # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd785aa01c3ef4..9a139e5b727531 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ aiolifx==0.8.6 aiolifx_effects==0.3.0 # homeassistant.components.lifx -aiolifx_themes==0.1.1 +aiolifx_themes==0.2.0 # homeassistant.components.lookin aiolookin==0.1.1 From eec1015789839e4713d5901be3f7b91cfdc1cca2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Oct 2022 00:38:03 -0500 Subject: [PATCH 009/394] Bump nexia to 2.0.5 (#81061) fixes #80988 --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 77280b1f503ca9..78576e06b8aff0 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.4"], + "requirements": ["nexia==2.0.5"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index d08127cc55805f..bbaed415da66c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1135,7 +1135,7 @@ nettigo-air-monitor==1.4.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.4 +nexia==2.0.5 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a139e5b727531..0c7b4d8a9a6f76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -825,7 +825,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.4.2 # homeassistant.components.nexia -nexia==2.0.4 +nexia==2.0.5 # homeassistant.components.discord nextcord==2.0.0a8 From a50fd6a259742caab5ab739ef9458cd0f351c5ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Thu, 27 Oct 2022 14:12:51 +0200 Subject: [PATCH 010/394] Update blebox_uniapi to 2.1.3 (#81071) fix: #80124 blebox_uniapi dependency version bump --- homeassistant/components/blebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 328f15abdac6d8..78c7186eb31177 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.1.0"], + "requirements": ["blebox_uniapi==2.1.3"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] diff --git a/requirements_all.txt b/requirements_all.txt index bbaed415da66c7..047e767f606310 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ bleak-retry-connector==2.4.2 bleak==0.19.0 # homeassistant.components.blebox -blebox_uniapi==2.1.0 +blebox_uniapi==2.1.3 # homeassistant.components.blink blinkpy==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c7b4d8a9a6f76..e3c4fccb6e19a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ bleak-retry-connector==2.4.2 bleak==0.19.0 # homeassistant.components.blebox -blebox_uniapi==2.1.0 +blebox_uniapi==2.1.3 # homeassistant.components.blink blinkpy==0.19.2 From cbd5e919cbddbb6afd5ac4582ed15fd6fcff1958 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 27 Oct 2022 20:42:16 +0200 Subject: [PATCH 011/394] Clean up superfluous Netatmo API calls (#81095) --- homeassistant/components/netatmo/data_handler.py | 5 ++++- homeassistant/components/netatmo/netatmo_entity_base.py | 8 +++++--- tests/components/netatmo/test_camera.py | 2 +- tests/components/netatmo/test_init.py | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index a376e6ee1875fd..15d776c45294b1 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -252,7 +252,7 @@ async def unsubscribe( self, signal_name: str, update_callback: CALLBACK_TYPE | None ) -> None: """Unsubscribe from publisher.""" - if update_callback in self.publisher[signal_name].subscriptions: + if update_callback not in self.publisher[signal_name].subscriptions: return self.publisher[signal_name].subscriptions.remove(update_callback) @@ -288,6 +288,9 @@ async def async_dispatch(self) -> None: person.entity_id: person.pseudo for person in home.persons.values() } + await self.unsubscribe(WEATHER, None) + await self.unsubscribe(AIR_CARE, None) + def setup_air_care(self) -> None: """Set up home coach/air care modules.""" for module in self.account.modules.values(): diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index d0359d739fdba9..c434d370e277de 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -63,9 +63,11 @@ async def async_added_to_hass(self) -> None: publisher["name"], signal_name, self.async_update_callback ) - for sub in self.data_handler.publisher[signal_name].subscriptions: - if sub is None: - await self.data_handler.unsubscribe(signal_name, None) + if any( + sub is None + for sub in self.data_handler.publisher[signal_name].subscriptions + ): + await self.data_handler.unsubscribe(signal_name, None) registry = dr.async_get(self.hass) if device := registry.async_get_device({(DOMAIN, self._id)}): diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 027b0907d50aa4..beb91c7565e2d3 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -472,7 +472,7 @@ async def fake_post_no_data(*args, **kwargs): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert fake_post_hits == 9 + assert fake_post_hits == 11 async def test_camera_image_raises_exception(hass, config_entry, requests_mock): diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 187a89afeb65b2..65cc991ec67445 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -110,7 +110,7 @@ async def fake_post(*args, **kwargs): await hass.async_block_till_done() - assert fake_post_hits == 8 + assert fake_post_hits == 10 mock_impl.assert_called_once() mock_webhook.assert_called_once() From 43164b5751e6eac914fab81431552263e96e49b9 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 27 Oct 2022 17:37:52 +0200 Subject: [PATCH 012/394] Bring back Netatmo force update code (#81098) --- homeassistant/components/netatmo/data_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 15d776c45294b1..1a322f8d8dbccc 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -176,8 +176,8 @@ async def async_update(self, event_time: datetime) -> None: @callback def async_force_update(self, signal_name: str) -> None: """Prioritize data retrieval for given data class entry.""" - # self.publisher[signal_name].next_scan = time() - # self._queue.rotate(-(self._queue.index(self.publisher[signal_name]))) + self.publisher[signal_name].next_scan = time() + self._queue.rotate(-(self._queue.index(self.publisher[signal_name]))) async def handle_event(self, event: dict) -> None: """Handle webhook events.""" From 8751eaaf3ee94011e5c961a96063024f58a39e8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Oct 2022 14:38:53 -0500 Subject: [PATCH 013/394] Bump dbus-fast to 1.51.0 (#81109) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a3348a1611b04c..60b260baf36a1a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.49.0" + "dbus-fast==1.51.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a4ccf0f58e888..d4a2dbc438bc4a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.49.0 +dbus-fast==1.51.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 047e767f606310..be8effeff8135c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.49.0 +dbus-fast==1.51.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3c4fccb6e19a2..b6d067d25270f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.49.0 +dbus-fast==1.51.0 # homeassistant.components.debugpy debugpy==1.6.3 From 4927f4206aa534479f95a8a403f14c2286cc3e97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Oct 2022 14:38:42 -0500 Subject: [PATCH 014/394] Add support for oralb IO Series 4 (#81110) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/oralb/__init__.py | 11 ++++++++ tests/components/oralb/test_config_flow.py | 21 ++++++++++++++- tests/components/oralb/test_sensor.py | 27 +++++++++++++++++++- 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index b3dfedde532aab..bf6879733f5aab 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.5.0"], + "requirements": ["oralb-ble==0.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index be8effeff8135c..f8004c149e9ed3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.5.0 +oralb-ble==0.6.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6d067d25270f5..fd84b1e511d1cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.5.0 +oralb-ble==0.6.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 diff --git a/tests/components/oralb/__init__.py b/tests/components/oralb/__init__.py index 567b9d7328ed95..5525a859f21c80 100644 --- a/tests/components/oralb/__init__.py +++ b/tests/components/oralb/__init__.py @@ -22,3 +22,14 @@ service_data={}, source="local", ) + + +ORALB_IO_SERIES_4_SERVICE_INFO = BluetoothServiceInfo( + name="GXB772CD\x00\x00\x00\x00\x00\x00\x00\x00\x00", + address="78:DB:2F:C2:48:BE", + rssi=-63, + manufacturer_data={220: b"\x074\x0c\x038\x00\x00\x02\x01\x00\x04"}, + service_uuids=[], + service_data={}, + source="local", +) diff --git a/tests/components/oralb/test_config_flow.py b/tests/components/oralb/test_config_flow.py index e4af11faddb24f..cb7f97a50898c8 100644 --- a/tests/components/oralb/test_config_flow.py +++ b/tests/components/oralb/test_config_flow.py @@ -6,7 +6,7 @@ from homeassistant.components.oralb.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType -from . import NOT_ORALB_SERVICE_INFO, ORALB_SERVICE_INFO +from . import NOT_ORALB_SERVICE_INFO, ORALB_IO_SERIES_4_SERVICE_INFO, ORALB_SERVICE_INFO from tests.common import MockConfigEntry @@ -30,6 +30,25 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" +async def test_async_step_bluetooth_valid_io_series4_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=ORALB_IO_SERIES_4_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.oralb.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IO Series 4 48BE" + assert result2["data"] == {} + assert result2["result"].unique_id == "78:DB:2F:C2:48:BE" + + async def test_async_step_bluetooth_not_oralb(hass): """Test discovery via bluetooth not oralb.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 4e37005f65a7ae..2122ad9bbff803 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.oralb.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME -from . import ORALB_SERVICE_INFO +from . import ORALB_IO_SERIES_4_SERVICE_INFO, ORALB_SERVICE_INFO from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -38,3 +38,28 @@ async def test_sensors(hass, entity_registry_enabled_by_default): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_sensors_io_series_4(hass, entity_registry_enabled_by_default): + """Test setting up creates the sensors with an io series 4.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=ORALB_IO_SERIES_4_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, ORALB_IO_SERIES_4_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 8 + + toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_mode") + toothbrush_sensor_attrs = toothbrush_sensor.attributes + assert toothbrush_sensor.state == "gum care" + assert toothbrush_sensor_attrs[ATTR_FRIENDLY_NAME] == "IO Series 4 48BE Mode" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 233ad2b90b38e6b169dcbd1002e5ef1eeb5b3e36 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 27 Oct 2022 22:00:34 +0200 Subject: [PATCH 015/394] Migrate KNX to use kelvin for color temperature (#81112) --- homeassistant/components/knx/light.py | 61 +++++++++++++-------------- tests/components/knx/test_light.py | 36 ++++++++++++---- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 9268b53581b753..e4260f5e868b8e 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -9,7 +9,7 @@ from homeassistant import config_entries from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, @@ -153,15 +153,8 @@ class KNXLight(KnxEntity, LightEntity): def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of KNX light.""" super().__init__(_create_light(xknx, config)) - self._max_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] - self._min_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] - - self._attr_max_mireds = color_util.color_temperature_kelvin_to_mired( - self._min_kelvin - ) - self._attr_min_mireds = color_util.color_temperature_kelvin_to_mired( - self._max_kelvin - ) + self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] + self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = self._device_unique_id() @@ -242,21 +235,23 @@ def xy_color(self) -> tuple[float, float] | None: return None @property - def color_temp(self) -> int | None: - """Return the color temperature in mireds.""" + def color_temp_kelvin(self) -> int | None: + """Return the color temperature in Kelvin.""" if self._device.supports_color_temperature: - kelvin = self._device.current_color_temperature - # Avoid division by zero if actuator reported 0 Kelvin (e.g., uninitialized DALI-Gateway) - if kelvin is not None and kelvin > 0: - return color_util.color_temperature_kelvin_to_mired(kelvin) + if kelvin := self._device.current_color_temperature: + return kelvin if self._device.supports_tunable_white: relative_ct = self._device.current_tunable_white if relative_ct is not None: - # as KNX devices typically use Kelvin we use it as base for - # calculating ct from percent - return color_util.color_temperature_kelvin_to_mired( - self._min_kelvin - + ((relative_ct / 255) * (self._max_kelvin - self._min_kelvin)) + return int( + self._attr_min_color_temp_kelvin + + ( + (relative_ct / 255) + * ( + self._attr_max_color_temp_kelvin + - self._attr_min_color_temp_kelvin + ) + ) ) return None @@ -288,7 +283,7 @@ def supported_color_modes(self) -> set | None: async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - mireds = kwargs.get(ATTR_COLOR_TEMP) + color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN) rgb = kwargs.get(ATTR_RGB_COLOR) rgbw = kwargs.get(ATTR_RGBW_COLOR) hs_color = kwargs.get(ATTR_HS_COLOR) @@ -297,7 +292,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: if ( not self.is_on and brightness is None - and mireds is None + and color_temp is None and rgb is None and rgbw is None and hs_color is None @@ -335,17 +330,21 @@ async def set_color( await set_color(rgb, None, brightness) return - if mireds is not None: - kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) - kelvin = min(self._max_kelvin, max(self._min_kelvin, kelvin)) - + if color_temp is not None: + color_temp = min( + self._attr_max_color_temp_kelvin, + max(self._attr_min_color_temp_kelvin, color_temp), + ) if self._device.supports_color_temperature: - await self._device.set_color_temperature(kelvin) + await self._device.set_color_temperature(color_temp) elif self._device.supports_tunable_white: - relative_ct = int( + relative_ct = round( 255 - * (kelvin - self._min_kelvin) - / (self._max_kelvin - self._min_kelvin) + * (color_temp - self._attr_min_color_temp_kelvin) + / ( + self._attr_max_color_temp_kelvin + - self._attr_min_color_temp_kelvin + ) ) await self._device.set_tunable_white(relative_ct) diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 56cf5b2c00a6c5..2f7484fad8b117 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -11,7 +11,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, + ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, ATTR_RGBW_COLOR, ColorMode, @@ -166,19 +166,25 @@ async def test_light_color_temp_absolute(hass: HomeAssistant, knx: KNXTestKit): brightness=255, color_mode=ColorMode.COLOR_TEMP, color_temp=370, + color_temp_kelvin=2700, ) # change color temperature from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_COLOR_TEMP: 250}, # 4000 Kelvin - 0x0FA0 + {"entity_id": "light.test", ATTR_COLOR_TEMP_KELVIN: 4000}, # 4000 - 0x0FA0 blocking=True, ) await knx.assert_write(test_ct, (0x0F, 0xA0)) knx.assert_state("light.test", STATE_ON, color_temp=250) # change color temperature from KNX await knx.receive_write(test_ct_state, (0x17, 0x70)) # 6000 Kelvin - 166 Mired - knx.assert_state("light.test", STATE_ON, color_temp=166) + knx.assert_state( + "light.test", + STATE_ON, + color_temp=166, + color_temp_kelvin=6000, + ) async def test_light_color_temp_relative(hass: HomeAssistant, knx: KNXTestKit): @@ -222,19 +228,33 @@ async def test_light_color_temp_relative(hass: HomeAssistant, knx: KNXTestKit): brightness=255, color_mode=ColorMode.COLOR_TEMP, color_temp=250, + color_temp_kelvin=4000, ) # change color temperature from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_COLOR_TEMP: 300}, # 3333 Kelvin - 33 % - 0x54 + { + "entity_id": "light.test", + ATTR_COLOR_TEMP_KELVIN: 3333, # 3333 Kelvin - 33.3 % - 0x55 + }, blocking=True, ) - await knx.assert_write(test_ct, (0x54,)) - knx.assert_state("light.test", STATE_ON, color_temp=300) + await knx.assert_write(test_ct, (0x55,)) + knx.assert_state( + "light.test", + STATE_ON, + color_temp=300, + color_temp_kelvin=3333, + ) # change color temperature from KNX - await knx.receive_write(test_ct_state, (0xE6,)) # 3900 Kelvin - 90 % - 256 Mired - knx.assert_state("light.test", STATE_ON, color_temp=256) + await knx.receive_write(test_ct_state, (0xE6,)) # 3901 Kelvin - 90.1 % - 256 Mired + knx.assert_state( + "light.test", + STATE_ON, + color_temp=256, + color_temp_kelvin=3901, + ) async def test_light_hs_color(hass: HomeAssistant, knx: KNXTestKit): From 6d973a1793f2edf718e18bd18cb018b95630829b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 27 Oct 2022 23:13:43 +0200 Subject: [PATCH 016/394] Update frontend to 20221027.0 (#81114) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 390b0ccfc182d0..c8d3645435f0d9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221026.0"], + "requirements": ["home-assistant-frontend==20221027.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d4a2dbc438bc4a..79e6340ed411bb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.51.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221026.0 +home-assistant-frontend==20221027.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index f8004c149e9ed3..78ee7727e286b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221026.0 +home-assistant-frontend==20221027.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd84b1e511d1cb..f177eae163616d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221026.0 +home-assistant-frontend==20221027.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 1c8a7fe8e8d1e57f47c9b50ab482212424fd8808 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 27 Oct 2022 17:30:32 -0400 Subject: [PATCH 017/394] Bumped version to 2022.11.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c194782ed29819..d9964b32b9b39d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index a869da99baa752..cf6598589aecdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b0" +version = "2022.11.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From aeecc93ad653cb7504bed01ff1074cb14b3b4721 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 29 Oct 2022 03:01:53 +0200 Subject: [PATCH 018/394] Allow empty string for filters for waze_travel_time (#80953) * Allow empty string for filters Signed-off-by: Kevin Stillhammer * Apply PR feedback Signed-off-by: Kevin Stillhammer Signed-off-by: Kevin Stillhammer --- .../waze_travel_time/config_flow.py | 2 +- .../components/waze_travel_time/sensor.py | 4 +- .../waze_travel_time/test_config_flow.py | 54 +++++++++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index fd6747cc1c8bd7..b26732e4cb1faf 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -52,7 +52,7 @@ async def async_step_init(self, user_input=None) -> FlowResult: if user_input is not None: return self.async_create_entry( title="", - data={k: v for k, v in user_input.items() if v not in (None, "")}, + data=user_input, ) return self.async_show_form( diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 942c1bccb36b05..c8d3e308435aa9 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -185,14 +185,14 @@ def update(self): ) routes = params.calc_all_routes_info(real_time=realtime) - if incl_filter is not None: + if incl_filter not in {None, ""}: routes = { k: v for k, v in routes.items() if incl_filter.lower() in k.lower() } - if excl_filter is not None: + if excl_filter not in {None, ""}: routes = { k: v for k, v in routes.items() diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index 51bf1ae831905c..d58f8d9a34d42f 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -19,6 +19,7 @@ IMPERIAL_UNITS, ) from homeassistant.const import CONF_NAME, CONF_REGION +from homeassistant.core import HomeAssistant from .const import MOCK_CONFIG @@ -26,7 +27,7 @@ @pytest.mark.usefixtures("validate_config_entry") -async def test_minimum_fields(hass): +async def test_minimum_fields(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -50,7 +51,7 @@ async def test_minimum_fields(hass): } -async def test_options(hass): +async def test_options(hass: HomeAssistant) -> None: """Test options flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -105,7 +106,7 @@ async def test_options(hass): @pytest.mark.usefixtures("validate_config_entry") -async def test_dupe(hass): +async def test_dupe(hass: HomeAssistant) -> None: """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -138,7 +139,9 @@ async def test_dupe(hass): @pytest.mark.usefixtures("invalidate_config_entry") -async def test_invalid_config_entry(hass, caplog): +async def test_invalid_config_entry( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -154,3 +157,46 @@ async def test_invalid_config_entry(hass, caplog): assert result2["errors"] == {"base": "cannot_connect"} assert "Error trying to validate entry" in caplog.text + + +@pytest.mark.usefixtures("mock_update") +async def test_reset_filters(hass: HomeAssistant) -> None: + """Test resetting inclusive and exclusive filters to empty string.""" + options = {**DEFAULT_OPTIONS} + options[CONF_INCL_FILTER] = "test" + options[CONF_EXCL_FILTER] = "test" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=options, entry_id="test" + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + config_entry.entry_id, data=None + ) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "", + CONF_INCL_FILTER: "", + CONF_REALTIME: False, + CONF_UNITS: IMPERIAL_UNITS, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + + assert config_entry.options == { + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "", + CONF_INCL_FILTER: "", + CONF_REALTIME: False, + CONF_UNITS: IMPERIAL_UNITS, + CONF_VEHICLE_TYPE: "taxi", + } From 1ef9e9e19aa3a8577f6467c3a85c04a70e4b0cc5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 28 Oct 2022 19:48:27 +0300 Subject: [PATCH 019/394] Fix Shelly Plus H&T sleep period on external power state change (#81121) --- .../components/shelly/coordinator.py | 30 ++++++++++++++++++- homeassistant/components/shelly/utils.py | 5 ++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 014355116c10ec..23f905b0fd939f 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -41,7 +41,12 @@ SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) -from .utils import device_update_info, get_block_device_name, get_rpc_device_name +from .utils import ( + device_update_info, + get_block_device_name, + get_rpc_device_name, + get_rpc_device_wakeup_period, +) @dataclass @@ -355,6 +360,24 @@ async def _async_reload_entry(self) -> None: LOGGER.debug("Reloading entry %s", self.name) await self.hass.config_entries.async_reload(self.entry.entry_id) + def update_sleep_period(self) -> bool: + """Check device sleep period & update if changed.""" + if ( + not self.device.initialized + or not (wakeup_period := get_rpc_device_wakeup_period(self.device.status)) + or wakeup_period == self.entry.data.get(CONF_SLEEP_PERIOD) + ): + return False + + data = {**self.entry.data} + data[CONF_SLEEP_PERIOD] = wakeup_period + self.hass.config_entries.async_update_entry(self.entry, data=data) + + update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period + self.update_interval = timedelta(seconds=update_interval) + + return True + @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -365,6 +388,8 @@ def _async_device_updates_handler(self) -> None: ): return + self.update_sleep_period() + self._last_event = self.device.event for event in self.device.event["events"]: @@ -393,6 +418,9 @@ def _async_device_updates_handler(self) -> None: async def _async_update_data(self) -> None: """Fetch data.""" + if self.update_sleep_period(): + return + if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 79f5a5848f0402..c3b6d24752f591 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -272,6 +272,11 @@ def get_rpc_device_sleep_period(config: dict[str, Any]) -> int: return cast(int, config["sys"].get("sleep", {}).get("wakeup_period", 0)) +def get_rpc_device_wakeup_period(status: dict[str, Any]) -> int: + """Return the device wakeup period in seconds or 0 for non sleeping devices.""" + return cast(int, status["sys"].get("wakeup_period", 0)) + + def get_info_auth(info: dict[str, Any]) -> bool: """Return true if device has authorization enabled.""" return cast(bool, info.get("auth") or info.get("auth_en")) From 3f55d037f813775155d1237b00e414596ce2085f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 05:31:50 -0500 Subject: [PATCH 020/394] Bump oralb-ble to 0.8.0 (#81123) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index bf6879733f5aab..e25f407add1102 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.6.0"], + "requirements": ["oralb-ble==0.8.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 78ee7727e286b5..b69582d458edb6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.6.0 +oralb-ble==0.8.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f177eae163616d..3b2557660f0c7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.6.0 +oralb-ble==0.8.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 9de89c97a4be8c918f6c3f7d8ae155e7dfd9e2b1 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Fri, 28 Oct 2022 13:02:33 +0200 Subject: [PATCH 021/394] Bump pyoverkiz to 1.5.6 (#81129) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index f09142c86f0669..d19495d82a2aca 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.5.5"], + "requirements": ["pyoverkiz==1.5.6"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index b69582d458edb6..4a81c2ef08912c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1789,7 +1789,7 @@ pyotgw==2.1.1 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.5 +pyoverkiz==1.5.6 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b2557660f0c7d..a6c67d0dd2e19e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1266,7 +1266,7 @@ pyotgw==2.1.1 pyotp==2.7.0 # homeassistant.components.overkiz -pyoverkiz==1.5.5 +pyoverkiz==1.5.6 # homeassistant.components.openweathermap pyowm==3.2.0 From 2bfd4e79d23e5d1b26f555b61e3de9216d85a382 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 12:05:48 -0500 Subject: [PATCH 022/394] Bump aiohomekit to 2.2.6 (#81144) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 24b2eebe61575b..34d47d6d835154 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.5"], + "requirements": ["aiohomekit==2.2.6"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4a81c2ef08912c..40fdb8475ceb3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.5 +aiohomekit==2.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6c67d0dd2e19e..c964bcb29b745d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.5 +aiohomekit==2.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http From 4dc2d885cfb38ba40504ba38f6eb0879c0bf32fe Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 29 Oct 2022 05:05:21 +0300 Subject: [PATCH 023/394] Add diagnostics to Switcher (#81146) --- .../components/switcher_kis/diagnostics.py | 28 +++++++++ .../switcher_kis/test_diagnostics.py | 59 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 homeassistant/components/switcher_kis/diagnostics.py create mode 100644 tests/components/switcher_kis/test_diagnostics.py diff --git a/homeassistant/components/switcher_kis/diagnostics.py b/homeassistant/components/switcher_kis/diagnostics.py new file mode 100644 index 00000000000000..93b3c36bd21445 --- /dev/null +++ b/homeassistant/components/switcher_kis/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for Switcher.""" +from __future__ import annotations + +from dataclasses import asdict +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DATA_DEVICE, DOMAIN + +TO_REDACT = {"device_id", "ip_address", "mac_address"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + devices = hass.data[DOMAIN][DATA_DEVICE] + + return async_redact_data( + { + "entry": entry.as_dict(), + "devices": [asdict(devices[d].data) for d in devices], + }, + TO_REDACT, + ) diff --git a/tests/components/switcher_kis/test_diagnostics.py b/tests/components/switcher_kis/test_diagnostics.py new file mode 100644 index 00000000000000..8655ba7ee1f98e --- /dev/null +++ b/tests/components/switcher_kis/test_diagnostics.py @@ -0,0 +1,59 @@ +"""Tests for the diagnostics data provided by Switcher.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from . import init_integration +from .consts import DUMMY_WATER_HEATER_DEVICE + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, mock_bridge, monkeypatch +) -> None: + """Test diagnostics.""" + entry = await init_integration(hass) + device = DUMMY_WATER_HEATER_DEVICE + monkeypatch.setattr(device, "last_data_update", "2022-09-28T16:42:12.706017") + mock_bridge.mock_callbacks([device]) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == { + "devices": [ + { + "auto_shutdown": "02:00:00", + "device_id": REDACTED, + "device_state": { + "__type": "", + "repr": "", + }, + "device_type": { + "__type": "", + "repr": ")>", + }, + "electric_current": 12.8, + "ip_address": REDACTED, + "last_data_update": "2022-09-28T16:42:12.706017", + "mac_address": REDACTED, + "name": "Heater FE12", + "power_consumption": 2780, + "remaining_time": "01:29:32", + } + ], + "entry": { + "entry_id": entry.entry_id, + "version": 1, + "domain": "switcher_kis", + "title": "Mock Title", + "data": {}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": "switcher_kis", + "disabled_by": None, + }, + } From 089bbe839157d34e324fe035ecee1eb0ddc684f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 20:01:03 -0500 Subject: [PATCH 024/394] Bump dbus-fast to 1.54.0 (#81148) * Bump dbus-fast to 1.53.0 changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.51.0...v1.53.0 * 54 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 60b260baf36a1a..a706d777bc6a64 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.51.0" + "dbus-fast==1.54.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 79e6340ed411bb..d8aef2416167bd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.51.0 +dbus-fast==1.54.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 40fdb8475ceb3d..63da7ae10d0c6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.51.0 +dbus-fast==1.54.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c964bcb29b745d..597bafd4b282af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.51.0 +dbus-fast==1.54.0 # homeassistant.components.debugpy debugpy==1.6.3 From 09fc492d80c204259c2764a50d95beb148b0d843 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 28 Oct 2022 18:25:44 -0400 Subject: [PATCH 025/394] Bump aiopyarr to 22.10.0 (#81153) --- homeassistant/components/lidarr/manifest.json | 2 +- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/radarr/fixtures/movie.json | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index 7d4e9bcede76dc..4c07e0e17629af 100644 --- a/homeassistant/components/lidarr/manifest.json +++ b/homeassistant/components/lidarr/manifest.json @@ -2,7 +2,7 @@ "domain": "lidarr", "name": "Lidarr", "documentation": "https://www.home-assistant.io/integrations/lidarr", - "requirements": ["aiopyarr==22.9.0"], + "requirements": ["aiopyarr==22.10.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 5bc15b24069cd6..9b140def96ad12 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,7 +2,7 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "requirements": ["aiopyarr==22.9.0"], + "requirements": ["aiopyarr==22.10.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 0c5b68a794976e..daf9e20586b267 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.9.0"], + "requirements": ["aiopyarr==22.10.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 63da7ae10d0c6a..bafcf1dbfba1c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -237,7 +237,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.9.0 +aiopyarr==22.10.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 597bafd4b282af..00bafb48aba5b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -212,7 +212,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.9.0 +aiopyarr==22.10.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/tests/components/radarr/fixtures/movie.json b/tests/components/radarr/fixtures/movie.json index 0f974859631849..b33ff6fc481984 100644 --- a/tests/components/radarr/fixtures/movie.json +++ b/tests/components/radarr/fixtures/movie.json @@ -21,8 +21,8 @@ "sortTitle": "string", "sizeOnDisk": 0, "overview": "string", - "inCinemas": "string", - "physicalRelease": "string", + "inCinemas": "2020-11-06T00:00:00Z", + "physicalRelease": "2019-03-19T00:00:00Z", "images": [ { "coverType": "poster", @@ -50,7 +50,7 @@ "certification": "string", "genres": ["string"], "tags": [0], - "added": "string", + "added": "2018-12-28T05:56:49Z", "ratings": { "votes": 0, "value": 0 From 230993b7c0ae01947f50adc147a46e757ec66ec5 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Fri, 28 Oct 2022 23:32:57 +0100 Subject: [PATCH 026/394] Growatt version bump - fixes #80950 (#81161) --- homeassistant/components/growatt_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 4127b48ae64d42..f3f17804fc11a6 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.2.2"], + "requirements": ["growattServer==1.2.3"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/requirements_all.txt b/requirements_all.txt index bafcf1dbfba1c2..e01d30e9b13790 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.2 +growattServer==1.2.3 # homeassistant.components.google_sheets gspread==5.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00bafb48aba5b7..bd3bd8f97b86e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.2.2 +growattServer==1.2.3 # homeassistant.components.google_sheets gspread==5.5.0 From f5fe3ec50e74ee4edd4e6163c0228c0bb324e667 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 17:31:30 -0500 Subject: [PATCH 027/394] Bump aiohomekit to 2.2.7 (#81163) changelog: https://github.com/Jc2k/aiohomekit/compare/2.2.6...2.2.7 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 34d47d6d835154..5aaae67d1d301b 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.6"], + "requirements": ["aiohomekit==2.2.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index e01d30e9b13790..9f0ce1f59f209e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.6 +aiohomekit==2.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd3bd8f97b86e8..b31882c1b37423 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.6 +aiohomekit==2.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http From d52323784e4b226709355907f92ec4ef8832808b Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 28 Oct 2022 18:31:53 -0400 Subject: [PATCH 028/394] Bump zigpy to 0.51.5 (#81164) Bump zigpy from 0.51.4 to 0.51.5 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 1c86fe52c5e059..79980d763e7f15 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.84", "zigpy-deconz==0.19.0", - "zigpy==0.51.4", + "zigpy==0.51.5", "zigpy-xbee==0.16.2", "zigpy-zigate==0.10.2", "zigpy-znp==0.9.1" diff --git a/requirements_all.txt b/requirements_all.txt index 9f0ce1f59f209e..487b407a181aec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2628,7 +2628,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.4 +zigpy==0.51.5 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b31882c1b37423..2f6238b4c11af0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1823,7 +1823,7 @@ zigpy-zigate==0.10.2 zigpy-znp==0.9.1 # homeassistant.components.zha -zigpy==0.51.4 +zigpy==0.51.5 # homeassistant.components.zwave_js zwave-js-server-python==0.43.0 From 6000cc087be63441ef04f882fb76ecd61c3d3afc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Oct 2022 17:32:38 -0500 Subject: [PATCH 029/394] Bump oralb-ble to 0.9.0 (#81166) * Bump oralb-ble to 0.9.0 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.8.0...v0.9.0 * empty --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index e25f407add1102..8f6949468048c0 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.8.0"], + "requirements": ["oralb-ble==0.9.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 487b407a181aec..d5806b536a0cc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.8.0 +oralb-ble==0.9.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f6238b4c11af0..5b6298814d1b62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.8.0 +oralb-ble==0.9.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 6036443d4a9858462c0b75043d83f486df155171 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Oct 2022 22:08:26 -0400 Subject: [PATCH 030/394] Bumped version to 2022.11.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d9964b32b9b39d..5e864997636ca7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index cf6598589aecdb..9ef2808bf19a7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b1" +version = "2022.11.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6f3b7d009d4577a480d33d52c89f2f5dd52f92a9 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 28 Oct 2022 15:48:16 +0300 Subject: [PATCH 031/394] Add diagnostics to webostv (#81133) --- .../components/webostv/diagnostics.py | 52 ++++++++++++++++ tests/components/webostv/conftest.py | 2 + tests/components/webostv/test_diagnostics.py | 61 +++++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 homeassistant/components/webostv/diagnostics.py create mode 100644 tests/components/webostv/test_diagnostics.py diff --git a/homeassistant/components/webostv/diagnostics.py b/homeassistant/components/webostv/diagnostics.py new file mode 100644 index 00000000000000..ce62f51b540e7a --- /dev/null +++ b/homeassistant/components/webostv/diagnostics.py @@ -0,0 +1,52 @@ +"""Diagnostics support for LG webOS Smart TV.""" +from __future__ import annotations + +from typing import Any + +from aiowebostv import WebOsClient + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from .const import DATA_CONFIG_ENTRY, DOMAIN + +TO_REDACT = { + CONF_CLIENT_SECRET, + CONF_UNIQUE_ID, + CONF_HOST, + "device_id", + "deviceUUID", + "icon", + "largeIcon", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + client: WebOsClient = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].client + + client_data = { + "is_registered": client.is_registered(), + "is_connected": client.is_connected(), + "current_app_id": client.current_app_id, + "current_channel": client.current_channel, + "apps": client.apps, + "inputs": client.inputs, + "system_info": client.system_info, + "software_info": client.software_info, + "hello_info": client.hello_info, + "sound_output": client.sound_output, + "is_on": client.is_on, + } + + return async_redact_data( + { + "entry": entry.as_dict(), + "client": client_data, + }, + TO_REDACT, + ) diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index 05f1be66d00cb9..c8333c844472bf 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -39,6 +39,8 @@ def client_fixture(): client.sound_output = "speaker" client.muted = False client.is_on = True + client.is_registered = Mock(return_value=True) + client.is_connected = Mock(return_value=True) async def mock_state_update_callback(): await client.register_state_update_callback.call_args[0][0](client) diff --git a/tests/components/webostv/test_diagnostics.py b/tests/components/webostv/test_diagnostics.py new file mode 100644 index 00000000000000..707f83b2fcf11c --- /dev/null +++ b/tests/components/webostv/test_diagnostics.py @@ -0,0 +1,61 @@ +"""Tests for the diagnostics data provided by LG webOS Smart TV.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from . import setup_webostv + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, hass_client: ClientSession, client +) -> None: + """Test diagnostics.""" + entry = await setup_webostv(hass) + assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == { + "client": { + "is_registered": True, + "is_connected": True, + "current_app_id": "com.webos.app.livetv", + "current_channel": { + "channelId": "ch1id", + "channelName": "Channel 1", + "channelNumber": "1", + }, + "apps": { + "com.webos.app.livetv": { + "icon": REDACTED, + "id": "com.webos.app.livetv", + "largeIcon": REDACTED, + "title": "Live TV", + } + }, + "inputs": { + "in1": {"appId": "app0", "id": "in1", "label": "Input01"}, + "in2": {"appId": "app1", "id": "in2", "label": "Input02"}, + }, + "system_info": {"modelName": "TVFAKE"}, + "software_info": {"major_ver": "major", "minor_ver": "minor"}, + "hello_info": {"deviceUUID": "**REDACTED**"}, + "sound_output": "speaker", + "is_on": True, + }, + "entry": { + "entry_id": entry.entry_id, + "version": 1, + "domain": "webostv", + "title": "fake_webos", + "data": { + "client_secret": "**REDACTED**", + "host": "**REDACTED**", + }, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + }, + } From 1b7524a79e80dec127a4993b7a1842bf7993d5f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2022 14:26:12 -0400 Subject: [PATCH 032/394] SSDP to allow more URLs (#81171) Co-authored-by: J. Nick Koston --- homeassistant/components/ssdp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 195bebb8321e82..d081ef877dee36 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -697,7 +697,7 @@ async def _async_start_upnp_servers(self) -> None: udn = await self._async_get_instance_udn() system_info = await async_get_system_info(self.hass) model_name = system_info["installation_type"] - presentation_url = get_url(self.hass) + presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False) serial_number = await async_get_instance_id(self.hass) HassUpnpServiceDevice.DEVICE_DEFINITION = ( HassUpnpServiceDevice.DEVICE_DEFINITION._replace( From 85545e9740df0714388360520da6947077173fe6 Mon Sep 17 00:00:00 2001 From: mezz64 <2854333+mezz64@users.noreply.github.com> Date: Sat, 29 Oct 2022 03:09:12 -0400 Subject: [PATCH 033/394] Bump pyEight to 0.3.2 (#81172) Co-authored-by: J. Nick Koston --- homeassistant/components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index c1833b222dfaa2..4f97b99b2e7975 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.3.0"], + "requirements": ["pyeight==0.3.2"], "codeowners": ["@mezz64", "@raman325"], "iot_class": "cloud_polling", "loggers": ["pyeight"], diff --git a/requirements_all.txt b/requirements_all.txt index d5806b536a0cc8..8d8e134b191419 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.eight_sleep -pyeight==0.3.0 +pyeight==0.3.2 # homeassistant.components.emby pyemby==1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b6298814d1b62..15bf428459d925 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1081,7 +1081,7 @@ pyeconet==0.1.15 pyefergy==22.1.1 # homeassistant.components.eight_sleep -pyeight==0.3.0 +pyeight==0.3.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 3323bf4ae9062481bfeeda6d3ba2ce971dc58dca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 28 Oct 2022 23:58:02 -0400 Subject: [PATCH 034/394] Set date in test to fixed one (#81175) --- tests/components/history_stats/test_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index b384b7c730b05c..6bae61b5fd8376 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1388,7 +1388,9 @@ def _fake_states(*args, **kwargs): async def test_end_time_with_microseconds_zeroed(time_zone, recorder_mock, hass): """Test the history statistics sensor that has the end time microseconds zeroed out.""" hass.config.set_time_zone(time_zone) - start_of_today = dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_of_today = dt_util.now().replace( + day=9, month=7, year=1986, hour=0, minute=0, second=0, microsecond=0 + ) start_time = start_of_today + timedelta(minutes=60) t0 = start_time + timedelta(minutes=20) t1 = t0 + timedelta(minutes=10) From 2dd8797f671e86d63f5d0d3ff311cf7f408bea28 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 02:40:40 -0500 Subject: [PATCH 035/394] Bump dbus-fast to 1.56.0 (#81177) * Bump dbus-fast to 1.56.0 Addes optimized readers for manufacturer data and interfaces added messages changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.55.0...v1.56.0 * empty --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a706d777bc6a64..3ac8ac513c1063 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.54.0" + "dbus-fast==1.56.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d8aef2416167bd..b50deca16bd880 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.54.0 +dbus-fast==1.56.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8d8e134b191419..ff133e8e0c4bc1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.54.0 +dbus-fast==1.56.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15bf428459d925..00648b45794186 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.54.0 +dbus-fast==1.56.0 # homeassistant.components.debugpy debugpy==1.6.3 From 43b1dd54d577428527f6eac06e6ce102c29183b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Sat, 29 Oct 2022 17:04:05 +0200 Subject: [PATCH 036/394] Bump pysma to 0.7.2 (#81188) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index c65f3b81d3b216..83bf4258a95df9 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.7.1"], + "requirements": ["pysma==0.7.2"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling", "loggers": ["pysma"] diff --git a/requirements_all.txt b/requirements_all.txt index ff133e8e0c4bc1..1f0b35e9706b1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1890,7 +1890,7 @@ pysignalclirestapi==0.3.18 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.7.1 +pysma==0.7.2 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00648b45794186..c1ec57f8d6751f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1331,7 +1331,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.18 # homeassistant.components.sma -pysma==0.7.1 +pysma==0.7.2 # homeassistant.components.smappee pysmappee==0.2.29 From 62635c2a96d30a926010b05ef9474b223e08faa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 13:22:46 -0500 Subject: [PATCH 037/394] Bump dbus-fast to 1.58.0 (#81195) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 3ac8ac513c1063..0db0433de2b199 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.4.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.56.0" + "dbus-fast==1.58.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b50deca16bd880..2d33e11a547f38 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.56.0 +dbus-fast==1.58.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1f0b35e9706b1c..ff96787ca76655 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.56.0 +dbus-fast==1.58.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1ec57f8d6751f..4e996aebab6654 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.56.0 +dbus-fast==1.58.0 # homeassistant.components.debugpy debugpy==1.6.3 From bf04f94e0535b49fed9d6981f2d2482eca65a4eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 13:25:35 -0500 Subject: [PATCH 038/394] Update to bleak 0.19.1 and bleak-retry-connector 2.5.0 (#81198) --- homeassistant/components/bluetooth/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0db0433de2b199..442759382d7ff5 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,8 +6,8 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.19.0", - "bleak-retry-connector==2.4.2", + "bleak==0.19.1", + "bleak-retry-connector==2.5.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.58.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2d33e11a547f38..e8e520c29bac02 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,8 +10,8 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.4.2 -bleak==0.19.0 +bleak-retry-connector==2.5.0 +bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index ff96787ca76655..a099a826559d04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,10 +413,10 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.4.2 +bleak-retry-connector==2.5.0 # homeassistant.components.bluetooth -bleak==0.19.0 +bleak==0.19.1 # homeassistant.components.blebox blebox_uniapi==2.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e996aebab6654..d81b9483727aca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,10 +337,10 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.4.2 +bleak-retry-connector==2.5.0 # homeassistant.components.bluetooth -bleak==0.19.0 +bleak==0.19.1 # homeassistant.components.blebox blebox_uniapi==2.1.3 From 16fe7df19e2c295f682860a089a7118473fe291b Mon Sep 17 00:00:00 2001 From: Menco Bolt Date: Sat, 29 Oct 2022 20:25:46 +0200 Subject: [PATCH 039/394] Today's Consumption is INCREASING (#81204) Co-authored-by: Paulus Schoutsen --- homeassistant/components/enphase_envoy/const.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/const.py b/homeassistant/components/enphase_envoy/const.py index c79c3af604bf5b..7c4931685269b4 100644 --- a/homeassistant/components/enphase_envoy/const.py +++ b/homeassistant/components/enphase_envoy/const.py @@ -34,7 +34,7 @@ key="seven_days_production", name="Last Seven Days Energy Production", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( @@ -54,14 +54,14 @@ key="daily_consumption", name="Today's Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( key="seven_days_consumption", name="Last Seven Days Energy Consumption", native_unit_of_measurement=ENERGY_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, device_class=SensorDeviceClass.ENERGY, ), SensorEntityDescription( From d0a0285dd9b90f75a84dccd0f4d0df62772d3a26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 14:05:59 -0500 Subject: [PATCH 040/394] Restore homekit_controller BLE broadcast_key from disk (#81211) * Restore homekit_controller BLE broadcast_key from disk Some accessories will sleep for a long time and only send broadcasted events which makes them have very long connection intervals to save battery. Since we need to connect to get a new broadcast key we now save the broadcast key between restarts to ensure we can decrypt the advertisments coming in even though we cannot make a connection to the device during startup. When we get a disconnected event later we will try again to connect and the device will be awake which will trigger a full sync * bump bump --- .../homekit_controller/config_flow.py | 3 ++- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/storage.py | 27 ++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 16 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 62144077a94719..da4ccfe9f9a8af 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -15,7 +15,7 @@ from aiohomekit.exceptions import AuthenticationError from aiohomekit.model.categories import Categories from aiohomekit.model.status_flags import StatusFlags -from aiohomekit.utils import domain_supported, domain_to_name +from aiohomekit.utils import domain_supported, domain_to_name, serialize_broadcast_key import voluptuous as vol from homeassistant import config_entries @@ -577,6 +577,7 @@ async def _entry_from_accessory(self, pairing: AbstractPairing) -> FlowResult: pairing.id, accessories_state.config_num, accessories_state.accessories.serialize(), + serialize_broadcast_key(accessories_state.broadcast_key), ) return self.async_create_entry(title=name, data=pairing_data) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5aaae67d1d301b..224b24f6077c67 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.7"], + "requirements": ["aiohomekit==2.2.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 51d8ce4ffd30a7..a5afb07620ae19 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -3,7 +3,9 @@ from __future__ import annotations import logging -from typing import Any, TypedDict +from typing import Any + +from aiohomekit.characteristic_cache import Pairing, StorageLayout from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store @@ -16,19 +18,6 @@ _LOGGER = logging.getLogger(__name__) -class Pairing(TypedDict): - """A versioned map of entity metadata as presented by aiohomekit.""" - - config_num: int - accessories: list[Any] - - -class StorageLayout(TypedDict): - """Cached pairing metadata needed by aiohomekit.""" - - pairings: dict[str, Pairing] - - class EntityMapStorage: """ Holds a cache of entity structure data from a paired HomeKit device. @@ -67,11 +56,17 @@ def get_map(self, homekit_id: str) -> Pairing | None: @callback def async_create_or_update_map( - self, homekit_id: str, config_num: int, accessories: list[Any] + self, + homekit_id: str, + config_num: int, + accessories: list[Any], + broadcast_key: str | None = None, ) -> Pairing: """Create a new pairing cache.""" _LOGGER.debug("Creating or updating entity map for %s", homekit_id) - data = Pairing(config_num=config_num, accessories=accessories) + data = Pairing( + config_num=config_num, accessories=accessories, broadcast_key=broadcast_key + ) self.storage_data[homekit_id] = data self._async_schedule_save() return data diff --git a/requirements_all.txt b/requirements_all.txt index a099a826559d04..13904065e33534 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.7 +aiohomekit==2.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d81b9483727aca..ec6e1853a3be86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.7 +aiohomekit==2.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 7e740b7c9d7cfa8b546fcf556c720e625fa4b30a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Oct 2022 14:06:17 -0500 Subject: [PATCH 041/394] Bump dbus-fast to 1.59.0 (#81215) * Bump dbus-fast to 1.59.0 changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.58.0...v1.59.0 * empty --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 442759382d7ff5..a5ea8c171d8d69 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.5.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.58.0" + "dbus-fast==1.59.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e8e520c29bac02..413a86be041d85 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.58.0 +dbus-fast==1.59.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 13904065e33534..a4469cfef3be18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.58.0 +dbus-fast==1.59.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec6e1853a3be86..e0388c86ed9540 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.58.0 +dbus-fast==1.59.0 # homeassistant.components.debugpy debugpy==1.6.3 From 96cdb2975566cfb6da59cada48553f990d2fe62f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 29 Oct 2022 15:07:25 -0400 Subject: [PATCH 042/394] Bumped version to 2022.11.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5e864997636ca7..3f25ea89c09e0c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 9ef2808bf19a7e..5a9507f8dfa226 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b2" +version = "2022.11.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a1eec7b55dd8dc1e271bc078e34eace86f8d3c73 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Mon, 31 Oct 2022 00:33:06 +0800 Subject: [PATCH 043/394] Expose NO2 and VOCs sensors to homekit (#81217) Co-authored-by: J. Nick Koston --- .../components/homekit/accessories.py | 4 + homeassistant/components/homekit/const.py | 2 + .../components/homekit/type_sensors.py | 64 +++++++++- homeassistant/components/homekit/util.py | 34 ++++- .../homekit/test_get_accessories.py | 12 ++ tests/components/homekit/test_type_sensors.py | 116 ++++++++++++++++-- 6 files changed, 218 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 61c2e3cd5ddd63..7d0de1a5740132 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -200,6 +200,10 @@ def get_accessory( # noqa: C901 or SensorDeviceClass.PM25 in state.entity_id ): a_type = "PM25Sensor" + elif device_class == SensorDeviceClass.NITROGEN_DIOXIDE: + a_type = "NitrogenDioxideSensor" + elif device_class == SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: + a_type = "VolatileOrganicCompoundsSensor" elif ( device_class == SensorDeviceClass.GAS or SensorDeviceClass.GAS in state.entity_id diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 264801c521faed..58e1e13a3f3bec 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -196,6 +196,7 @@ CHAR_MOTION_DETECTED = "MotionDetected" CHAR_MUTE = "Mute" CHAR_NAME = "Name" +CHAR_NITROGEN_DIOXIDE_DENSITY = "NitrogenDioxideDensity" CHAR_OBSTRUCTION_DETECTED = "ObstructionDetected" CHAR_OCCUPANCY_DETECTED = "OccupancyDetected" CHAR_ON = "On" @@ -226,6 +227,7 @@ CHAR_HOLD_POSITION = "HoldPosition" CHAR_TEMP_DISPLAY_UNITS = "TemperatureDisplayUnits" CHAR_VALVE_TYPE = "ValveType" +CHAR_VOC_DENSITY = "VOCDensity" CHAR_VOLUME = "Volume" CHAR_VOLUME_SELECTOR = "VolumeSelector" CHAR_VOLUME_CONTROL_TYPE = "VolumeControlType" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index e877ffff07acb6..4e9c897dff98c4 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -33,10 +33,12 @@ CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, CHAR_MOTION_DETECTED, + CHAR_NITROGEN_DIOXIDE_DENSITY, CHAR_OCCUPANCY_DETECTED, CHAR_PM10_DENSITY, CHAR_PM25_DENSITY, CHAR_SMOKE_DETECTED, + CHAR_VOC_DENSITY, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, SERV_CARBON_DIOXIDE_SENSOR, @@ -55,7 +57,9 @@ from .util import ( convert_to_float, density_to_air_quality, + density_to_air_quality_nitrogen_dioxide, density_to_air_quality_pm10, + density_to_air_quality_voc, temperature_to_homekit, ) @@ -206,7 +210,7 @@ def create_services(self): def async_update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if not density: + if density is None: return if self.char_density.value != density: self.char_density.set_value(density) @@ -233,7 +237,7 @@ def create_services(self): def async_update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if not density: + if density is None: return if self.char_density.value != density: self.char_density.set_value(density) @@ -244,6 +248,62 @@ def async_update_state(self, new_state): _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) +@TYPES.register("NitrogenDioxideSensor") +class NitrogenDioxideSensor(AirQualitySensor): + """Generate a NitrogenDioxideSensor accessory as NO2 sensor.""" + + def create_services(self): + """Override the init function for PM 2.5 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_NITROGEN_DIOXIDE_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char( + CHAR_NITROGEN_DIOXIDE_DENSITY, value=0 + ) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if density is None: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_nitrogen_dioxide(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + +@TYPES.register("VolatileOrganicCompoundsSensor") +class VolatileOrganicCompoundsSensor(AirQualitySensor): + """Generate a VolatileOrganicCompoundsSensor accessory as VOCs sensor.""" + + def create_services(self): + """Override the init function for PM 2.5 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_VOC_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_VOC_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if density is None: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_voc(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + @TYPES.register("CarbonMonoxideSensor") class CarbonMonoxideSensor(HomeAccessory): """Generate a CarbonMonoxidSensor accessory as CO sensor.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index ee02ea1a576f8c..413786c22c4d9c 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -413,14 +413,40 @@ def density_to_air_quality(density: float) -> int: def density_to_air_quality_pm10(density: float) -> int: - """Map PM10 density to HomeKit AirQuality level.""" - if density <= 40: + """Map PM10 µg/m3 density to HomeKit AirQuality level.""" + if density <= 54: # US AQI 0-50 (HomeKit: Excellent) return 1 + if density <= 154: # US AQI 51-100 (HomeKit: Good) + return 2 + if density <= 254: # US AQI 101-150 (HomeKit: Fair) + return 3 + if density <= 354: # US AQI 151-200 (HomeKit: Inferior) + return 4 + return 5 # US AQI 201+ (HomeKit: Poor) + + +def density_to_air_quality_nitrogen_dioxide(density: float) -> int: + """Map nitrogen dioxide µg/m3 to HomeKit AirQuality level.""" + if density <= 30: + return 1 + if density <= 60: + return 2 if density <= 80: + return 3 + if density <= 90: + return 4 + return 5 + + +def density_to_air_quality_voc(density: float) -> int: + """Map VOCs µg/m3 to HomeKit AirQuality level.""" + if density <= 24: + return 1 + if density <= 48: return 2 - if density <= 120: + if density <= 64: return 3 - if density <= 300: + if density <= 96: return 4 return 5 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 32f4abe98f174b..12113ada5cb77f 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -226,6 +226,18 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): "40", {ATTR_DEVICE_CLASS: "pm25"}, ), + ( + "NitrogenDioxideSensor", + "sensor.air_quality_nitrogen_dioxide", + "50", + {ATTR_DEVICE_CLASS: SensorDeviceClass.NITROGEN_DIOXIDE}, + ), + ( + "VolatileOrganicCompoundsSensor", + "sensor.air_quality_volatile_organic_compounds", + "55", + {ATTR_DEVICE_CLASS: SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS}, + ), ( "CarbonMonoxideSensor", "sensor.co", diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 4997a35910d8aa..28dfe04932f80e 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -15,9 +15,11 @@ CarbonMonoxideSensor, HumiditySensor, LightSensor, + NitrogenDioxideSensor, PM10Sensor, PM25Sensor, TemperatureSensor, + VolatileOrganicCompoundsSensor, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -155,24 +157,24 @@ async def test_pm10(hass, hk_driver): assert acc.char_density.value == 0 assert acc.char_quality.value == 0 - hass.states.async_set(entity_id, "34") + hass.states.async_set(entity_id, "54") await hass.async_block_till_done() - assert acc.char_density.value == 34 + assert acc.char_density.value == 54 assert acc.char_quality.value == 1 - hass.states.async_set(entity_id, "70") + hass.states.async_set(entity_id, "154") await hass.async_block_till_done() - assert acc.char_density.value == 70 + assert acc.char_density.value == 154 assert acc.char_quality.value == 2 - hass.states.async_set(entity_id, "110") + hass.states.async_set(entity_id, "254") await hass.async_block_till_done() - assert acc.char_density.value == 110 + assert acc.char_density.value == 254 assert acc.char_quality.value == 3 - hass.states.async_set(entity_id, "200") + hass.states.async_set(entity_id, "354") await hass.async_block_till_done() - assert acc.char_density.value == 200 + assert acc.char_density.value == 354 assert acc.char_quality.value == 4 hass.states.async_set(entity_id, "400") @@ -228,6 +230,104 @@ async def test_pm25(hass, hk_driver): assert acc.char_quality.value == 5 +async def test_no2(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_nitrogen_dioxide" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = NitrogenDioxideSensor( + hass, hk_driver, "Nitrogen Dioxide Sensor", entity_id, 2, None + ) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "30") + await hass.async_block_till_done() + assert acc.char_density.value == 30 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "60") + await hass.async_block_till_done() + assert acc.char_density.value == 60 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "80") + await hass.async_block_till_done() + assert acc.char_density.value == 80 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "90") + await hass.async_block_till_done() + assert acc.char_density.value == 90 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "100") + await hass.async_block_till_done() + assert acc.char_density.value == 100 + assert acc.char_quality.value == 5 + + +async def test_voc(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_volatile_organic_compounds" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = VolatileOrganicCompoundsSensor( + hass, hk_driver, "Volatile Organic Compounds Sensor", entity_id, 2, None + ) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "24") + await hass.async_block_till_done() + assert acc.char_density.value == 24 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "48") + await hass.async_block_till_done() + assert acc.char_density.value == 48 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "64") + await hass.async_block_till_done() + assert acc.char_density.value == 64 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "96") + await hass.async_block_till_done() + assert acc.char_density.value == 96 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "128") + await hass.async_block_till_done() + assert acc.char_density.value == 128 + assert acc.char_quality.value == 5 + + async def test_co(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = "sensor.co" From ec038835f63c012688838e67dbf1120d2b8462ef Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 30 Oct 2022 20:01:10 +0100 Subject: [PATCH 044/394] Catch `ApiError` while checking credentials in NAM integration (#81243) * Catch ApiError while checking credentials * Update tests * Suggested change --- homeassistant/components/nam/__init__.py | 2 ++ tests/components/nam/test_init.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 25615db6eede6e..0fbc93846345c2 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -56,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await nam.async_check_credentials() + except ApiError as err: + raise ConfigEntryNotReady from err except AuthFailed as err: raise ConfigEntryAuthFailed from err diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index b6f278d4e94846..a6d11305599204 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -32,12 +32,30 @@ async def test_config_not_ready(hass): unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) + entry.add_to_hass(hass) with patch( "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=ApiError("API Error"), ): - entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_not_ready_while_checking_credentials(hass): + """Test for setup failure if the connection fails while checking credentials.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + side_effect=ApiError("API Error"), + ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY @@ -50,12 +68,12 @@ async def test_config_auth_failed(hass): unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) + entry.add_to_hass(hass) with patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailed("Authorization has failed"), ): - entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_ERROR From 03f74b32341fe74d0d080943be7ada39f57533b6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 30 Oct 2022 22:46:16 +0100 Subject: [PATCH 045/394] Bump pyatmo to 7.3.0 (#81271) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 8beb7bc521a7d3..436b6329c1da65 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.2.0"], + "requirements": ["pyatmo==7.3.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index cc8200f00f9dc6..fd452c7ad9aab0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 396b0063911c02..74a6fb35137395 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1030,7 +1030,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.apple_tv pyatv==0.10.3 From ba8fd6b01e09fe558041b3cc2a185b953b36e2d1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 30 Oct 2022 15:07:10 -0700 Subject: [PATCH 046/394] Google calendar test cleanup, avoiding dupe config entry setup (#81256) --- tests/components/google/test_calendar.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 3bd584f4c6fa72..90ec8f44850ac6 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -62,15 +62,11 @@ @pytest.fixture(autouse=True) def mock_test_setup( - hass, test_api_calendar, mock_calendars_list, - config_entry, ): - """Fixture that pulls in the default fixtures for tests in this file.""" + """Fixture that sets up the default API responses during integration setup.""" mock_calendars_list({"items": [test_api_calendar]}) - config_entry.add_to_hass(hass) - return def get_events_url(entity: str, start: str, end: str) -> str: From 1106df158dd2fe4b7443a40131a6b216fb538d2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 17:38:09 -0500 Subject: [PATCH 047/394] Bump bleak-retry-connector to 2.6.0 (#81270) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f8d1867035d376..261b4480671098 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.5.0", + "bleak-retry-connector==2.6.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d48b85e346b80c..6762357d58d029 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index fd452c7ad9aab0..b47a16ff875744 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74a6fb35137395..bfd4149cc41b44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 # homeassistant.components.bluetooth bleak==0.19.1 From 11d7e1e45fbabf0a73ea27472ae156c6948f98ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 17:43:09 -0500 Subject: [PATCH 048/394] Provide a human readable error when an esphome ble proxy connection fails (#81266) --- homeassistant/components/esphome/bluetooth/client.py | 12 +++++++++++- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 9094186226f185..cffeae8d3eb3ed 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,6 +7,7 @@ from typing import Any, TypeVar, cast import uuid +from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic @@ -182,8 +183,17 @@ def _on_bluetooth_connection_state( return if error: + try: + ble_connection_error = BLEConnectionError(error) + ble_connection_error_name = ble_connection_error.name + human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] + except (KeyError, ValueError): + ble_connection_error_name = str(error) + human_error = f"Unknown error code {error}" connected_future.set_exception( - BleakError(f"Error while connecting: {error}") + BleakError( + f"Error {ble_connection_error_name} while connecting: {human_error}" + ) ) return diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ab33ed8585a661..c0230ce841084c 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==11.2.0"], + "requirements": ["aioesphomeapi==11.3.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index b47a16ff875744..b3846580ded921 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.2.0 +aioesphomeapi==11.3.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfd4149cc41b44..83169340cfe23a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.2.0 +aioesphomeapi==11.3.0 # homeassistant.components.flo aioflo==2021.11.0 From c8a3392471ee00633103bcb081b2c3d305251752 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 18:02:54 -0500 Subject: [PATCH 049/394] Move esphome gatt services cache to be per device (#81265) --- .../components/esphome/bluetooth/client.py | 6 +++--- .../components/esphome/domain_data.py | 20 ------------------- .../components/esphome/entry_data.py | 20 ++++++++++++++++++- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index cffeae8d3eb3ed..918d93f3d2cb6c 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -265,9 +265,9 @@ async def get_services( A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ address_as_int = self._address_as_int - domain_data = self.domain_data + entry_data = self.entry_data if dangerous_use_bleak_cache and ( - cached_services := domain_data.get_gatt_services_cache(address_as_int) + cached_services := entry_data.get_gatt_services_cache(address_as_int) ): _LOGGER.debug( "Cached services hit for %s - %s", @@ -311,7 +311,7 @@ async def get_services( self._ble_device.name, self._ble_device.address, ) - domain_data.set_gatt_services_cache(address_as_int, services) + entry_data.set_gatt_services_cache(address_as_int, services) return services def _resolve_characteristic( diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index acaa76185e76a8..01f0a4d6b1369b 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -1,13 +1,9 @@ """Support for esphome domain data.""" from __future__ import annotations -from collections.abc import MutableMapping from dataclasses import dataclass, field from typing import TypeVar, cast -from bleak.backends.service import BleakGATTServiceCollection -from lru import LRU # pylint: disable=no-name-in-module - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder @@ -17,7 +13,6 @@ STORAGE_VERSION = 1 DOMAIN = "esphome" -MAX_CACHED_SERVICES = 128 _DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") @@ -29,21 +24,6 @@ class DomainData: _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) _stores: dict[str, Store] = field(default_factory=dict) _entry_by_unique_id: dict[str, ConfigEntry] = field(default_factory=dict) - _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( - default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] - ) - - def get_gatt_services_cache( - self, address: int - ) -> BleakGATTServiceCollection | None: - """Get the BleakGATTServiceCollection for the given address.""" - return self._gatt_services_cache.get(address) - - def set_gatt_services_cache( - self, address: int, services: BleakGATTServiceCollection - ) -> None: - """Set the BleakGATTServiceCollection for the given address.""" - self._gatt_services_cache[address] = services def get_by_unique_id(self, unique_id: str) -> ConfigEntry: """Get the config entry by its unique ID.""" diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index ac2a148d89913d..5d474b0fb15855 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, MutableMapping from dataclasses import dataclass, field import logging from typing import Any, cast @@ -30,6 +30,8 @@ UserService, ) from aioesphomeapi.model import ButtonInfo +from bleak.backends.service import BleakGATTServiceCollection +from lru import LRU # pylint: disable=no-name-in-module from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -57,6 +59,7 @@ SwitchInfo: Platform.SWITCH, TextSensorInfo: Platform.SENSOR, } +MAX_CACHED_SERVICES = 128 @dataclass @@ -92,6 +95,21 @@ class RuntimeEntryData: _ble_connection_free_futures: list[asyncio.Future[int]] = field( default_factory=list ) + _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( + default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] + ) + + def get_gatt_services_cache( + self, address: int + ) -> BleakGATTServiceCollection | None: + """Get the BleakGATTServiceCollection for the given address.""" + return self._gatt_services_cache.get(address) + + def set_gatt_services_cache( + self, address: int, services: BleakGATTServiceCollection + ) -> None: + """Set the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache[address] = services @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: From 4899f1d6327ad76a3157a5a9fedf9fb69b5f7562 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 Oct 2022 00:27:12 +0100 Subject: [PATCH 050/394] Revert 81271 (#81275) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 436b6329c1da65..8beb7bc521a7d3 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.3.0"], + "requirements": ["pyatmo==7.2.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index b3846580ded921..6a7d7b18ba6e0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.2.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83169340cfe23a..4cf57dc39c8fac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1030,7 +1030,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.2.0 # homeassistant.components.apple_tv pyatv==0.10.3 From 7e47aff316786a656244d87fd5c938c603774403 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 19:24:14 -0500 Subject: [PATCH 051/394] Bump aioesphomeapi to 11.4.0 (#81277) --- 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 c0230ce841084c..cab81882788b8b 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==11.3.0"], + "requirements": ["aioesphomeapi==11.4.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 6a7d7b18ba6e0c..9679c5a32faad5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.3.0 +aioesphomeapi==11.4.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4cf57dc39c8fac..8f8abc44030405 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.3.0 +aioesphomeapi==11.4.0 # homeassistant.components.flo aioflo==2021.11.0 From 4fb6fa9cca21067f1011745a38ec9f43fbf8d334 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 19:24:32 -0500 Subject: [PATCH 052/394] Bump bleak-retry-connector to 2.7.0 (#81280) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 261b4480671098..6b799e94e55650 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.6.0", + "bleak-retry-connector==2.7.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6762357d58d029..f75f7ba60da9ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 9679c5a32faad5..c9203f8962eb8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f8abc44030405..4aacb6ec9e67a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 # homeassistant.components.bluetooth bleak==0.19.1 From e63616987848772c10c2e5fbb607278e80b6bfc5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 31 Oct 2022 00:32:43 +0000 Subject: [PATCH 053/394] [ci skip] Translation update --- .../google_travel_time/translations/ca.json | 1 + .../google_travel_time/translations/de.json | 1 + .../google_travel_time/translations/es.json | 1 + .../google_travel_time/translations/et.json | 1 + .../google_travel_time/translations/hu.json | 1 + .../google_travel_time/translations/ru.json | 1 + .../components/ovo_energy/translations/es.json | 2 +- .../components/ovo_energy/translations/et.json | 1 + .../components/scrape/translations/de.json | 6 ++++++ .../components/scrape/translations/es.json | 6 ++++++ .../components/transmission/translations/de.json | 13 +++++++++++++ .../components/transmission/translations/es.json | 13 +++++++++++++ .../components/transmission/translations/et.json | 13 +++++++++++++ .../components/transmission/translations/ru.json | 13 +++++++++++++ 14 files changed, 72 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_travel_time/translations/ca.json b/homeassistant/components/google_travel_time/translations/ca.json index cfd24ef02a86a6..34ca784b3ec962 100644 --- a/homeassistant/components/google_travel_time/translations/ca.json +++ b/homeassistant/components/google_travel_time/translations/ca.json @@ -28,6 +28,7 @@ "mode": "Mode de transport", "time": "Temps", "time_type": "Tipus de temps", + "traffic_mode": "Mode tr\u00e0nsit", "transit_mode": "Tipus de transport", "transit_routing_preference": "Prefer\u00e8ncia de rutes de tr\u00e0nsit", "units": "Unitats" diff --git a/homeassistant/components/google_travel_time/translations/de.json b/homeassistant/components/google_travel_time/translations/de.json index 24bf9799ee0a60..13ecac74e3c4ab 100644 --- a/homeassistant/components/google_travel_time/translations/de.json +++ b/homeassistant/components/google_travel_time/translations/de.json @@ -28,6 +28,7 @@ "mode": "Reisemodus", "time": "Uhrzeit", "time_type": "Zeittyp", + "traffic_mode": "Verkehrsmodus", "transit_mode": "Transit-Modus", "transit_routing_preference": "Transit-Routing-Einstellungen", "units": "Einheiten" diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json index 54c8320665c858..508aee06b7b35d 100644 --- a/homeassistant/components/google_travel_time/translations/es.json +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -28,6 +28,7 @@ "mode": "Modo de viaje", "time": "Hora", "time_type": "Tipo de tiempo", + "traffic_mode": "Modo de tr\u00e1fico", "transit_mode": "Modo de tr\u00e1nsito", "transit_routing_preference": "Preferencia de ruta de tr\u00e1nsito", "units": "Unidades" diff --git a/homeassistant/components/google_travel_time/translations/et.json b/homeassistant/components/google_travel_time/translations/et.json index 0c8a90e89495a6..27adfb1e1b9243 100644 --- a/homeassistant/components/google_travel_time/translations/et.json +++ b/homeassistant/components/google_travel_time/translations/et.json @@ -28,6 +28,7 @@ "mode": "Reisimise viis", "time": "Aeg", "time_type": "Aja t\u00fc\u00fcp", + "traffic_mode": "S\u00f5iduvahend", "transit_mode": "Liikumisviis", "transit_routing_preference": "Teekonna eelistused", "units": "\u00dchikud" diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json index 3ad294f4fabef6..73d558858543cd 100644 --- a/homeassistant/components/google_travel_time/translations/hu.json +++ b/homeassistant/components/google_travel_time/translations/hu.json @@ -28,6 +28,7 @@ "mode": "Utaz\u00e1si m\u00f3d", "time": "Id\u0151", "time_type": "Id\u0151 t\u00edpusa", + "traffic_mode": "Forgalmi m\u00f3d", "transit_mode": "Tranzit m\u00f3d", "transit_routing_preference": "Tranzit \u00fatv\u00e1laszt\u00e1si be\u00e1ll\u00edt\u00e1s", "units": "Egys\u00e9gek" diff --git a/homeassistant/components/google_travel_time/translations/ru.json b/homeassistant/components/google_travel_time/translations/ru.json index d506ed4ca5e333..31494ff1c1d6b7 100644 --- a/homeassistant/components/google_travel_time/translations/ru.json +++ b/homeassistant/components/google_travel_time/translations/ru.json @@ -28,6 +28,7 @@ "mode": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f", "time": "\u0412\u0440\u0435\u043c\u044f", "time_type": "\u0422\u0438\u043f \u0432\u0440\u0435\u043c\u0435\u043d\u0438", + "traffic_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u0444\u0438\u043a\u0430", "transit_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u0430", "transit_routing_preference": "\u041f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043f\u043e \u0442\u0440\u0430\u043d\u0437\u0438\u0442\u043d\u043e\u043c\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0443", "units": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f" diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index 1a975921b25339..a41cff3a535d51 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -16,7 +16,7 @@ }, "user": { "data": { - "account": "ID de la cuenta OVO (s\u00f3lo a\u00f1adir si tienes varias cuentas)", + "account": "ID de la cuenta OVO (solo a\u00f1adir si tienes varias cuentas)", "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, diff --git a/homeassistant/components/ovo_energy/translations/et.json b/homeassistant/components/ovo_energy/translations/et.json index ab33d27f337cc5..f7ce2be8a12275 100644 --- a/homeassistant/components/ovo_energy/translations/et.json +++ b/homeassistant/components/ovo_energy/translations/et.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO konto ID (lisa ainult siis, kui on mitu kontot)", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, diff --git a/homeassistant/components/scrape/translations/de.json b/homeassistant/components/scrape/translations/de.json index d4e2f37f88dbb6..17aeb0816a1617 100644 --- a/homeassistant/components/scrape/translations/de.json +++ b/homeassistant/components/scrape/translations/de.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "Die Konfiguration von Scrape mit YAML wurde in den Integrationsschl\u00fcssel verschoben. \n\nDeine vorhandene YAML-Konfiguration funktioniert f\u00fcr zwei weitere Versionen.\n\nMigriere deine YAML-Konfiguration gem\u00e4\u00df der Dokumentation zum Integrationsschl\u00fcssel.", + "title": "Die Scrape-YAML-Konfiguration wurde verschoben" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/es.json b/homeassistant/components/scrape/translations/es.json index d88197473d3249..a001498c7efe43 100644 --- a/homeassistant/components/scrape/translations/es.json +++ b/homeassistant/components/scrape/translations/es.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "La configuraci\u00f3n de Scrape usando YAML se ha movido a la clave de integraci\u00f3n. \n\nTu configuraci\u00f3n YAML existente funcionar\u00e1 durante 2 versiones m\u00e1s. \n\nMigra tu configuraci\u00f3n YAML a la clave de integraci\u00f3n de acuerdo con la documentaci\u00f3n.", + "title": "La configuraci\u00f3n YAML de Scrape se ha movido" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index 04274f2c1cb58f..3f0a2364ce8abe 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/translations/de.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, und ersetze den Namensschl\u00fcssel durch den Entry_id-Schl\u00fcssel.", + "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" + } + } + }, + "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 30180811cb47d7..69242bda413216 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio y sustituye la clave nombre por la clave entry_id.", + "title": "Se va a eliminar la clave nombre en los servicios de Transmission" + } + } + }, + "title": "Se va a eliminar la clave nombre en los servicios de Transmission" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/et.json b/homeassistant/components/transmission/translations/et.json index 745ef1030afb51..3fab9d169db46a 100644 --- a/homeassistant/components/transmission/translations/et.json +++ b/homeassistant/components/transmission/translations/et.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte ja asenda nimev\u00f5ti v\u00f5tmega entry_id-ga.", + "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" + } + } + }, + "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index ba6787eed7d709..cfd1c7e0e849db 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 name \u043d\u0430 \u043a\u043b\u044e\u0447 entry_id.", + "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" + } + } + }, + "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" + } + }, "options": { "step": { "init": { From e709b74c3f836b271cc5141be2128e440a709812 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 20:39:34 -0500 Subject: [PATCH 054/394] Bump aioesphomeapi to 11.4.1 (#81282) --- 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 cab81882788b8b..c27e3b8dc3e114 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==11.4.0"], + "requirements": ["aioesphomeapi==11.4.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index c9203f8962eb8d..c0d58c5b6a1d20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.0 +aioesphomeapi==11.4.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4aacb6ec9e67a1..561bd9531c768c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.0 +aioesphomeapi==11.4.1 # homeassistant.components.flo aioflo==2021.11.0 From 1d94fbb176eb353c968aeeb6aa71bf4867bbf545 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 20:40:01 -0500 Subject: [PATCH 055/394] Bump bleak-retry-connector to 2.8.0 (#81283) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 6b799e94e55650..660345606c8788 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.7.0", + "bleak-retry-connector==2.8.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f75f7ba60da9ba..994a8d44019670 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index c0d58c5b6a1d20..5286d26793703b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 561bd9531c768c..91d62550d4e2d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 # homeassistant.components.bluetooth bleak==0.19.1 From ccefc510c37875824cb4c768c48753b2bda534ce Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 22:10:30 -0500 Subject: [PATCH 056/394] Do not fire the esphome ble disconnected callback if we were not connected (#81286) --- homeassistant/components/esphome/bluetooth/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 918d93f3d2cb6c..68f1788afdbf87 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -127,13 +127,15 @@ def _unsubscribe_connection_state(self) -> None: def _async_ble_device_disconnected(self) -> None: """Handle the BLE device disconnecting from the ESP.""" - _LOGGER.debug("%s: BLE device disconnected", self._source) - self._is_connected = False + was_connected = self._is_connected self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] + self._is_connected = False if self._disconnected_event: self._disconnected_event.set() self._disconnected_event = None - self._async_call_bleak_disconnected_callback() + if was_connected: + _LOGGER.debug("%s: BLE device disconnected", self._source) + self._async_call_bleak_disconnected_callback() self._unsubscribe_connection_state() def _async_esp_disconnected(self) -> None: From a766b41b13e6f1aae0e5c779061281fd87656605 Mon Sep 17 00:00:00 2001 From: aschmitz <29508+aschmitz@users.noreply.github.com> Date: Sun, 30 Oct 2022 22:50:46 -0500 Subject: [PATCH 057/394] Add basic Aranet integration (#80865) --- CODEOWNERS | 2 + homeassistant/components/aranet/__init__.py | 56 ++++ .../components/aranet/config_flow.py | 123 +++++++++ homeassistant/components/aranet/const.py | 3 + homeassistant/components/aranet/manifest.json | 23 ++ homeassistant/components/aranet/sensor.py | 169 ++++++++++++ homeassistant/components/aranet/strings.json | 25 ++ .../components/aranet/translations/en.json | 25 ++ homeassistant/generated/bluetooth.py | 12 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/aranet/__init__.py | 58 ++++ tests/components/aranet/conftest.py | 8 + tests/components/aranet/test_config_flow.py | 252 ++++++++++++++++++ tests/components/aranet/test_sensor.py | 111 ++++++++ 17 files changed, 880 insertions(+) create mode 100644 homeassistant/components/aranet/__init__.py create mode 100644 homeassistant/components/aranet/config_flow.py create mode 100644 homeassistant/components/aranet/const.py create mode 100644 homeassistant/components/aranet/manifest.json create mode 100644 homeassistant/components/aranet/sensor.py create mode 100644 homeassistant/components/aranet/strings.json create mode 100644 homeassistant/components/aranet/translations/en.json create mode 100644 tests/components/aranet/__init__.py create mode 100644 tests/components/aranet/conftest.py create mode 100644 tests/components/aranet/test_config_flow.py create mode 100644 tests/components/aranet/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index acc723a3493896..4012868c712616 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,8 @@ build.json @home-assistant/supervisor /tests/components/apprise/ @caronc /homeassistant/components/aprs/ @PhilRW /tests/components/aprs/ @PhilRW +/homeassistant/components/aranet/ @aschmitz +/tests/components/aranet/ @aschmitz /homeassistant/components/arcam_fmj/ @elupus /tests/components/arcam_fmj/ @elupus /homeassistant/components/arris_tg2492lg/ @vanbalken diff --git a/homeassistant/components/aranet/__init__.py b/homeassistant/components/aranet/__init__.py new file mode 100644 index 00000000000000..07e19ca2618648 --- /dev/null +++ b/homeassistant/components/aranet/__init__.py @@ -0,0 +1,56 @@ +"""The Aranet integration.""" +from __future__ import annotations + +import logging + +from aranet4.client import Aranet4Advertisement + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +def _service_info_to_adv( + service_info: BluetoothServiceInfoBleak, +) -> Aranet4Advertisement: + return Aranet4Advertisement(service_info.device, service_info.advertisement) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Aranet from a config entry.""" + + address = entry.unique_id + assert address is not None + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=_service_info_to_adv, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + 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): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/aranet/config_flow.py b/homeassistant/components/aranet/config_flow.py new file mode 100644 index 00000000000000..029ee251ae7920 --- /dev/null +++ b/homeassistant/components/aranet/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow for Aranet integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aranet4.client import Aranet4Advertisement, Version as AranetVersion +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import AbortFlow, FlowResult + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +MIN_VERSION = AranetVersion(1, 2, 0) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aranet.""" + + VERSION = 1 + + def __init__(self) -> None: + """Set up a new config flow for Aranet.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: Aranet4Advertisement | None = None + self._discovered_devices: dict[str, tuple[str, Aranet4Advertisement]] = {} + + def _raise_for_advertisement_errors(self, adv: Aranet4Advertisement) -> None: + """Raise any configuration errors that apply to an advertisement.""" + # Old versions of firmware don't expose sensor data in advertisements. + if not adv.manufacturer_data or adv.manufacturer_data.version < MIN_VERSION: + raise AbortFlow("outdated_version") + + # If integrations are disabled, we get no sensor data. + if not adv.manufacturer_data.integrations: + raise AbortFlow("integrations_disabled") + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the Bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + adv = Aranet4Advertisement(discovery_info.device, discovery_info.advertisement) + self._raise_for_advertisement_errors(adv) + + self._discovery_info = discovery_info + self._discovered_device = adv + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + adv = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = adv.readings.name if adv.readings else discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + adv = self._discovered_devices[address][1] + self._raise_for_advertisement_errors(adv) + + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address][0], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + + adv = Aranet4Advertisement( + discovery_info.device, discovery_info.advertisement + ) + if adv.manufacturer_data: + self._discovered_devices[address] = ( + adv.readings.name if adv.readings else discovery_info.name, + adv, + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + addr: dev[0] + for (addr, dev) in self._discovered_devices.items() + } + ) + } + ), + ) diff --git a/homeassistant/components/aranet/const.py b/homeassistant/components/aranet/const.py new file mode 100644 index 00000000000000..056c627daa87b8 --- /dev/null +++ b/homeassistant/components/aranet/const.py @@ -0,0 +1,3 @@ +"""Constants for the Aranet integration.""" + +DOMAIN = "aranet" diff --git a/homeassistant/components/aranet/manifest.json b/homeassistant/components/aranet/manifest.json new file mode 100644 index 00000000000000..6dc5cbe903c4df --- /dev/null +++ b/homeassistant/components/aranet/manifest.json @@ -0,0 +1,23 @@ +{ + "domain": "aranet", + "name": "Aranet", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aranet", + "requirements": ["aranet4==2.1.3"], + "dependencies": ["bluetooth"], + "codeowners": ["@aschmitz"], + "iot_class": "local_push", + "integration_type": "device", + "bluetooth": [ + { + "manufacturer_id": 1794, + "service_uuid": "f0cd1400-95da-4f4b-9ac8-aa55d312af0c", + "connectable": false + }, + { + "manufacturer_id": 1794, + "service_uuid": "0000fce0-0000-1000-8000-00805f9b34fb", + "connectable": false + } + ] +} diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py new file mode 100644 index 00000000000000..6d8c7feb0aca3a --- /dev/null +++ b/homeassistant/components/aranet/sensor.py @@ -0,0 +1,169 @@ +"""Support for Aranet sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from aranet4.client import Aranet4Advertisement +from bleak.backends.device import BLEDevice + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_NAME, + ATTR_SW_VERSION, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + PRESSURE_HPA, + TEMP_CELSIUS, + TIME_SECONDS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + "temperature": SensorEntityDescription( + key="temperature", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "humidity": SensorEntityDescription( + key="humidity", + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "pressure": SensorEntityDescription( + key="pressure", + name="Pressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + "co2": SensorEntityDescription( + key="co2", + name="Carbon Dioxide", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + "battery": SensorEntityDescription( + key="battery", + name="Battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "interval": SensorEntityDescription( + key="update_interval", + name="Update Interval", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_SECONDS, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device: BLEDevice, + key: str, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(key, device.address) + + +def _sensor_device_info_to_hass( + adv: Aranet4Advertisement, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if adv.readings and adv.readings.name: + hass_device_info[ATTR_NAME] = adv.readings.name + if adv.manufacturer_data: + hass_device_info[ATTR_SW_VERSION] = str(adv.manufacturer_data.version) + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + adv: Aranet4Advertisement, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a Bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={adv.device.address: _sensor_device_info_to_hass(adv)}, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(adv.device, key): desc + for key, desc in SENSOR_DESCRIPTIONS.items() + }, + entity_data={ + _device_key_to_bluetooth_entity_key(adv.device, key): getattr( + adv.readings, key, None + ) + for key in SENSOR_DESCRIPTIONS + }, + entity_names={ + _device_key_to_bluetooth_entity_key(adv.device, key): desc.name + for key, desc in SENSOR_DESCRIPTIONS.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Aranet sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + Aranet4BluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class Aranet4BluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of an Aranet sensor.""" + + @property + def available(self) -> bool: + """Return whether the entity was available in the last update.""" + # Our superclass covers "did the device disappear entirely", but if the + # device has smart home integrations disabled, it will send BLE beacons + # without data, which we turn into Nones here. Because None is never a + # valid value for any of the Aranet sensors, that means the entity is + # actually unavailable. + return ( + super().available + and self.processor.entity_data.get(self.entity_key) is not None + ) + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/aranet/strings.json b/homeassistant/components/aranet/strings.json new file mode 100644 index 00000000000000..1970beec21020f --- /dev/null +++ b/homeassistant/components/aranet/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "error": { + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "integrations_diabled": "This device doesn't have integrations enabled. Please enable smart home integrations using the app and try again.", + "no_devices_found": "No unconfigured Aranet devices found.", + "outdated_version": "This device is using outdated firmware. Please update it to at least v1.2.0 and try again." + } + } +} diff --git a/homeassistant/components/aranet/translations/en.json b/homeassistant/components/aranet/translations/en.json new file mode 100644 index 00000000000000..303fd56f1c8938 --- /dev/null +++ b/homeassistant/components/aranet/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "integrations_diabled": "This device doesn't have integrations enabled. Please enable smart home integrations using the app and try again.", + "no_devices_found": "No unconfigured Aranet4 devices found.", + "outdated_version": "This device is using outdated firmware. Please update it to at least v1.2.0 and try again." + }, + "error": { + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index c4dd22cef17846..4a0b9529ee7c25 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -9,6 +9,18 @@ "domain": "airthings_ble", "manufacturer_id": 820, }, + { + "domain": "aranet", + "manufacturer_id": 1794, + "service_uuid": "f0cd1400-95da-4f4b-9ac8-aa55d312af0c", + "connectable": False, + }, + { + "domain": "aranet", + "manufacturer_id": 1794, + "service_uuid": "0000fce0-0000-1000-8000-00805f9b34fb", + "connectable": False, + }, { "domain": "bluemaestro", "manufacturer_id": 307, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 772068401a5453..5279f34369127b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -31,6 +31,7 @@ "anthemav", "apcupsd", "apple_tv", + "aranet", "arcam_fmj", "aseko_pool_live", "asuswrt", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3ef8c87488232e..5e245ad9734804 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -313,6 +313,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "aranet": { + "name": "Aranet", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "arcam_fmj": { "name": "Arcam FMJ Receivers", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 5286d26793703b..e7996679c0967c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -335,6 +335,9 @@ aprslib==0.7.0 # homeassistant.components.aqualogic aqualogic==2.6 +# homeassistant.components.aranet +aranet4==2.1.3 + # homeassistant.components.arcam_fmj arcam-fmj==0.12.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91d62550d4e2d1..33db3ca737c7b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,6 +298,9 @@ apprise==1.1.0 # homeassistant.components.aprs aprslib==0.7.0 +# homeassistant.components.aranet +aranet4==2.1.3 + # homeassistant.components.arcam_fmj arcam-fmj==0.12.0 diff --git a/tests/components/aranet/__init__.py b/tests/components/aranet/__init__.py new file mode 100644 index 00000000000000..2fe27329bda14a --- /dev/null +++ b/tests/components/aranet/__init__.py @@ -0,0 +1,58 @@ +"""Tests for the Aranet integration.""" + +from time import time + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + + +def fake_service_info(name, service_uuid, manufacturer_data): + """Return a BluetoothServiceInfoBleak for use in testing.""" + return BluetoothServiceInfoBleak( + name=name, + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + manufacturer_data=manufacturer_data, + service_data={}, + service_uuids=[service_uuid], + source="local", + connectable=False, + time=time(), + device=BLEDevice("aa:bb:cc:dd:ee:ff", name=name), + advertisement=AdvertisementData( + local_name=name, + manufacturer_data=manufacturer_data, + service_data={}, + service_uuids=[service_uuid], + rssi=-60, + tx_power=-127, + platform_data=(), + ), + ) + + +NOT_ARANET4_SERVICE_INFO = fake_service_info( + "Not it", "61DE521B-F0BF-9F44-64D4-75BBE1738105", {3234: b"\x00\x01"} +) + +OLD_FIRMWARE_SERVICE_INFO = fake_service_info( + "Aranet4 12345", + "f0cd1400-95da-4f4b-9ac8-aa55d312af0c", + {1794: b"\x21\x0a\x04\x00\x00\x00\x00\x00"}, +) + +DISABLED_INTEGRATIONS_SERVICE_INFO = fake_service_info( + "Aranet4 12345", + "0000fce0-0000-1000-8000-00805f9b34fb", + {1794: b"\x01\x00\x02\x01\x00\x00\x00\x00"}, +) + +VALID_DATA_SERVICE_INFO = fake_service_info( + "Aranet4 12345", + "0000fce0-0000-1000-8000-00805f9b34fb", + { + 1794: b'\x21\x00\x02\x01\x00\x00\x00\x01\x8a\x02\xa5\x01\xb1&"Y\x01,\x01\xe8\x00\x88' + }, +) diff --git a/tests/components/aranet/conftest.py b/tests/components/aranet/conftest.py new file mode 100644 index 00000000000000..fca081d2e2ad37 --- /dev/null +++ b/tests/components/aranet/conftest.py @@ -0,0 +1,8 @@ +"""Aranet session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/aranet/test_config_flow.py b/tests/components/aranet/test_config_flow.py new file mode 100644 index 00000000000000..2b4172c30cd6ea --- /dev/null +++ b/tests/components/aranet/test_config_flow.py @@ -0,0 +1,252 @@ +"""Test the Aranet config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.aranet.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + DISABLED_INTEGRATIONS_SERVICE_INFO, + NOT_ARANET4_SERVICE_INFO, + OLD_FIRMWARE_SERVICE_INFO, + VALID_DATA_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet4 12345" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_aranet4(hass): + """Test that we reject discovery via Bluetooth for an unrelated device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_ARANET4_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet4 12345" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_user_no_devices_found(hass: HomeAssistant): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_only_other_devices_found(hass: HomeAssistant): + """Test setup from service info cache with only other devices found.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[NOT_ARANET4_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass: HomeAssistant): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet4 12345" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass: HomeAssistant): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass: HomeAssistant): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[VALID_DATA_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_old_firmware(hass: HomeAssistant): + """Test we can't set up a device with firmware too old to report measurements.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[OLD_FIRMWARE_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "outdated_version" + + +async def test_async_step_user_integrations_disabled(hass: HomeAssistant): + """Test we can't set up a device the device's integration setting disabled.""" + with patch( + "homeassistant.components.aranet.config_flow.async_discovered_service_info", + return_value=[DISABLED_INTEGRATIONS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "integrations_disabled" diff --git a/tests/components/aranet/test_sensor.py b/tests/components/aranet/test_sensor.py new file mode 100644 index 00000000000000..a0edcca4803b51 --- /dev/null +++ b/tests/components/aranet/test_sensor.py @@ -0,0 +1,111 @@ +"""Test the Aranet sensors.""" + + +from homeassistant.components.aranet.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import DISABLED_INTEGRATIONS_SERVICE_INFO, VALID_DATA_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, VALID_DATA_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + batt_sensor = hass.states.get("sensor.aranet4_12345_battery") + batt_sensor_attrs = batt_sensor.attributes + assert batt_sensor.state == "89" + assert batt_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Battery" + assert batt_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert batt_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + co2_sensor = hass.states.get("sensor.aranet4_12345_carbon_dioxide") + co2_sensor_attrs = co2_sensor.attributes + assert co2_sensor.state == "650" + assert co2_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Carbon Dioxide" + assert co2_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "ppm" + assert co2_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + humid_sensor = hass.states.get("sensor.aranet4_12345_humidity") + humid_sensor_attrs = humid_sensor.attributes + assert humid_sensor.state == "34" + assert humid_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Humidity" + assert humid_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert humid_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.aranet4_12345_temperature") + temp_sensor_attrs = temp_sensor.attributes + assert temp_sensor.state == "21.1" + assert temp_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Temperature" + assert temp_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + press_sensor = hass.states.get("sensor.aranet4_12345_pressure") + press_sensor_attrs = press_sensor.attributes + assert press_sensor.state == "990.5" + assert press_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Pressure" + assert press_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "hPa" + assert press_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + interval_sensor = hass.states.get("sensor.aranet4_12345_update_interval") + interval_sensor_attrs = interval_sensor.attributes + assert interval_sensor.state == "300" + assert interval_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Update Interval" + assert interval_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "s" + assert interval_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_smart_home_integration_disabled(hass): + """Test disabling smart home integration marks entities as unavailable.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, DISABLED_INTEGRATIONS_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + batt_sensor = hass.states.get("sensor.aranet4_12345_battery") + assert batt_sensor.state == "unavailable" + + co2_sensor = hass.states.get("sensor.aranet4_12345_carbon_dioxide") + assert co2_sensor.state == "unavailable" + + humid_sensor = hass.states.get("sensor.aranet4_12345_humidity") + assert humid_sensor.state == "unavailable" + + temp_sensor = hass.states.get("sensor.aranet4_12345_temperature") + assert temp_sensor.state == "unavailable" + + press_sensor = hass.states.get("sensor.aranet4_12345_pressure") + assert press_sensor.state == "unavailable" + + interval_sensor = hass.states.get("sensor.aranet4_12345_update_interval") + assert interval_sensor.state == "unavailable" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 1bdd8fff445e67fa50157c5087b20cd00277fc76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 00:28:38 -0500 Subject: [PATCH 058/394] Bump bleak-retry-connector to 2.8.1 (#81285) * Bump bleak-retry-connector to 2.8.1 reduces logging now that we have found the problem with esphome devices not disconnecting ble devices after timeout changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.0...v2.8.1 * empty --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 660345606c8788..091962fbc83294 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.0", + "bleak-retry-connector==2.8.1", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 994a8d44019670..914731a81649c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index e7996679c0967c..192db4138a6f36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -416,7 +416,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33db3ca737c7b4..cc5431c3d8163a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,7 +340,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 # homeassistant.components.bluetooth bleak==0.19.1 From 8db7afb2e0106f1f8a1f085d45d143024f77daf1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 00:31:37 -0500 Subject: [PATCH 059/394] Include esphome device name in BLE logs (#81284) * Include esphome device name in BLE logs This makes it easier to debug what is going on when there are multiple esphome proxies * revert unintended change --- homeassistant/components/esphome/__init__.py | 2 + .../components/esphome/bluetooth/__init__.py | 8 +-- .../components/esphome/bluetooth/client.py | 53 +++++++++++++++---- .../components/esphome/entry_data.py | 17 ++++-- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a5428f7d6c5fec..23b6a6550e4889 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -249,6 +249,8 @@ async def on_connect() -> None: async def on_disconnect() -> None: """Run disconnect callbacks on API disconnect.""" + name = entry_data.device_info.name if entry_data.device_info else host + _LOGGER.debug("%s: %s disconnected, running disconnected callbacks", name, host) for disconnect_cb in entry_data.disconnect_callbacks: disconnect_cb() entry_data.disconnect_callbacks = [] diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index b4d5fdbd04df14..b5be5362474858 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -30,13 +30,15 @@ def _async_can_connect_factory( @hass_callback def _async_can_connect() -> bool: """Check if a given source can make another connection.""" + can_connect = bool(entry_data.available and entry_data.ble_connections_free) _LOGGER.debug( - "Checking if %s can connect, available=%s, ble_connections_free=%s", + "%s: Checking can connect, available=%s, ble_connections_free=%s result=%s", source, entry_data.available, entry_data.ble_connections_free, + can_connect, ) - return bool(entry_data.available and entry_data.ble_connections_free) + return can_connect return _async_can_connect @@ -55,7 +57,7 @@ async def async_connect_scanner( version = entry_data.device_info.bluetooth_proxy_version connectable = version >= 2 _LOGGER.debug( - "Connecting scanner for %s, version=%s, connectable=%s", + "%s: Connecting scanner version=%s, connectable=%s", source, version, connectable, diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 68f1788afdbf87..5f20a73f4d6316 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -61,7 +61,7 @@ async def _async_wrap_bluetooth_connected_operation( if disconnected_event.is_set(): task.cancel() raise BleakError( - f"{self._ble_device.name} ({self._ble_device.address}): " # pylint: disable=protected-access + f"{self._source}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access "Disconnected during operation" ) return next(iter(done)).result() @@ -120,7 +120,10 @@ def _unsubscribe_connection_state(self) -> None: self._cancel_connection_state() except (AssertionError, ValueError) as ex: _LOGGER.debug( - "Failed to unsubscribe from connection state (likely connection dropped): %s", + "%s: %s - %s: Failed to unsubscribe from connection state (likely connection dropped): %s", + self._source, + self._ble_device.name, + self._ble_device.address, ex, ) self._cancel_connection_state = None @@ -134,13 +137,23 @@ def _async_ble_device_disconnected(self) -> None: self._disconnected_event.set() self._disconnected_event = None if was_connected: - _LOGGER.debug("%s: BLE device disconnected", self._source) + _LOGGER.debug( + "%s: %s - %s: BLE device disconnected", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self._async_call_bleak_disconnected_callback() self._unsubscribe_connection_state() def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from hass.""" - _LOGGER.debug("%s: ESP device disconnected", self._source) + _LOGGER.debug( + "%s: %s - %s: ESP device disconnected", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self.entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) self._async_ble_device_disconnected() @@ -170,7 +183,10 @@ def _on_bluetooth_connection_state( ) -> None: """Handle a connect or disconnect.""" _LOGGER.debug( - "Connection state changed: connected=%s mtu=%s error=%s", + "%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s", + self._source, + self._ble_device.name, + self._ble_device.address, connected, mtu, error, @@ -203,6 +219,12 @@ def _on_bluetooth_connection_state( connected_future.set_exception(BleakError("Disconnected")) return + _LOGGER.debug( + "%s: %s - %s: connected, registering for disconnected callbacks", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) connected_future.set_result(connected) @@ -230,7 +252,10 @@ async def _wait_for_free_connection_slot(self, timeout: float) -> None: if self.entry_data.ble_connections_free: return _LOGGER.debug( - "%s: Out of connection slots, waiting for a free one", self._source + "%s: %s - %s: Out of connection slots, waiting for a free one", + self._source, + self._ble_device.name, + self._ble_device.address, ) async with async_timeout.timeout(timeout): await self.entry_data.wait_for_ble_connections_free() @@ -272,20 +297,29 @@ async def get_services( cached_services := entry_data.get_gatt_services_cache(address_as_int) ): _LOGGER.debug( - "Cached services hit for %s - %s", + "%s: %s - %s: Cached services hit", + self._source, self._ble_device.name, self._ble_device.address, ) self.services = cached_services return self.services _LOGGER.debug( - "Cached services miss for %s - %s", + "%s: %s - %s: Cached services miss", + self._source, self._ble_device.name, self._ble_device.address, ) esphome_services = await self._client.bluetooth_gatt_get_services( address_as_int ) + _LOGGER.debug( + "%s: %s - %s: Got services: %s", + self._source, + self._ble_device.name, + self._ble_device.address, + esphome_services, + ) max_write_without_response = self.mtu_size - GATT_HEADER_SIZE services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] for service in esphome_services.services: @@ -309,7 +343,8 @@ async def get_services( ) self.services = services _LOGGER.debug( - "Cached services saved for %s - %s", + "%s: %s - %s: Cached services saved", + self._source, self._ble_device.name, self._ble_device.address, ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 5d474b0fb15855..faa9074b880e88 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -99,6 +99,11 @@ class RuntimeEntryData: default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] ) + @property + def name(self) -> str: + """Return the name of the device.""" + return self.device_info.name if self.device_info else self.entry_id + def get_gatt_services_cache( self, address: int ) -> BleakGATTServiceCollection | None: @@ -114,8 +119,13 @@ def set_gatt_services_cache( @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" - name = self.device_info.name if self.device_info else self.entry_id - _LOGGER.debug("%s: BLE connection limits: %s/%s", name, free, limit) + _LOGGER.debug( + "%s: BLE connection limits: used=%s free=%s limit=%s", + self.name, + limit - free, + free, + limit, + ) self.ble_connections_free = free self.ble_connections_limit = limit if free: @@ -186,7 +196,8 @@ def async_update_state(self, state: EntityState) -> None: subscription_key = (type(state), state.key) self.state[type(state)][state.key] = state _LOGGER.debug( - "Dispatching update with key %s: %s", + "%s: dispatching update with key %s: %s", + self.name, subscription_key, state, ) From 47a0f89adabf27c76cfffe4ff9dce77fe6457201 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 31 Oct 2022 03:23:05 -0500 Subject: [PATCH 060/394] Bump pyipp to 0.12.1 (#81287) bump pyipp to 0.12.1 --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index aadfdc8feeabb8..b673a2d5a6dccf 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -3,7 +3,7 @@ "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", "integration_type": "device", - "requirements": ["pyipp==0.12.0"], + "requirements": ["pyipp==0.12.1"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 192db4138a6f36..93cee8ab0285de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1634,7 +1634,7 @@ pyintesishome==1.8.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.12.0 +pyipp==0.12.1 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc5431c3d8163a..ae0bbe72346be6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1150,7 +1150,7 @@ pyinsteon==1.2.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.12.0 +pyipp==0.12.1 # homeassistant.components.iqvia pyiqvia==2022.04.0 From ae2419b56912abb3034dad5ca824b99b8a1adcdb Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 31 Oct 2022 10:22:45 +0100 Subject: [PATCH 061/394] Add support for PMSx003 sensors in NAM integration (#81289) * Add support for PMSx003 * Organize the order of tests --- homeassistant/components/nam/const.py | 6 ++ homeassistant/components/nam/sensor.py | 37 +++++++++ tests/components/nam/test_sensor.py | 111 ++++++++++++++++++++----- 3 files changed, 132 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index 2b6a74383b501e..5e18b94745c928 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -22,6 +22,12 @@ ATTR_HECA_HUMIDITY: Final = "heca_humidity" ATTR_HECA_TEMPERATURE: Final = "heca_temperature" ATTR_MHZ14A_CARBON_DIOXIDE: Final = "mhz14a_carbon_dioxide" +ATTR_PMSX003: Final = "pms" +ATTR_PMSX003_CAQI: Final = f"{ATTR_PMSX003}{SUFFIX_CAQI}" +ATTR_PMSX003_CAQI_LEVEL: Final = f"{ATTR_PMSX003}{SUFFIX_CAQI}_level" +ATTR_PMSX003_P0: Final = f"{ATTR_PMSX003}{SUFFIX_P0}" +ATTR_PMSX003_P1: Final = f"{ATTR_PMSX003}{SUFFIX_P1}" +ATTR_PMSX003_P2: Final = f"{ATTR_PMSX003}{SUFFIX_P2}" ATTR_SDS011: Final = "sds011" ATTR_SDS011_CAQI: Final = f"{ATTR_SDS011}{SUFFIX_CAQI}" ATTR_SDS011_CAQI_LEVEL: Final = f"{ATTR_SDS011}{SUFFIX_CAQI}_level" diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 01107baf31b0c7..ce3fdbf16a83a7 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -43,6 +43,11 @@ ATTR_HECA_HUMIDITY, ATTR_HECA_TEMPERATURE, ATTR_MHZ14A_CARBON_DIOXIDE, + ATTR_PMSX003_CAQI, + ATTR_PMSX003_CAQI_LEVEL, + ATTR_PMSX003_P0, + ATTR_PMSX003_P1, + ATTR_PMSX003_P2, ATTR_SDS011_CAQI, ATTR_SDS011_CAQI_LEVEL, ATTR_SDS011_P1, @@ -136,6 +141,38 @@ device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key=ATTR_PMSX003_CAQI, + name="PMSx003 CAQI", + icon="mdi:air-filter", + ), + SensorEntityDescription( + key=ATTR_PMSX003_CAQI_LEVEL, + name="PMSx003 CAQI level", + icon="mdi:air-filter", + device_class="nam__caqi_level", + ), + SensorEntityDescription( + key=ATTR_PMSX003_P0, + name="PMSx003 particulate matter 1.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_PMSX003_P1, + name="PMSx003 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_PMSX003_P2, + name="PMSx003 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key=ATTR_SDS011_CAQI, name="SDS011 CAQI", diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index bee4c515cd0b3c..dc9e9a76d76541 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -228,6 +228,73 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_caqi_level") + assert state + assert state.state == "very low" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_pmsx003_caqi_level") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi_level" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_caqi") + assert state + assert state.state == "19" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + + entry = registry.async_get("sensor.nettigo_air_monitor_pmsx003_caqi") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_caqi" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_particulate_matter_10") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get( + "sensor.nettigo_air_monitor_pmsx003_particulate_matter_10" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p1" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_particulate_matter_2_5") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get( + "sensor.nettigo_air_monitor_pmsx003_particulate_matter_2_5" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p2" + + state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_particulate_matter_1_0") + assert state + assert state.state == "6" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get( + "sensor.nettigo_air_monitor_pmsx003_particulate_matter_1_0" + ) + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-pms_p0" + state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_10") assert state assert state.state == "19" @@ -238,18 +305,20 @@ async def test_sensor(hass): == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER ) - entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi") + entry = registry.async_get( + "sensor.nettigo_air_monitor_sds011_particulate_matter_10" + ) assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi") assert state assert state.state == "19" assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi_level") + entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi") assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi_level" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi" state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi_level") assert state @@ -257,11 +326,9 @@ async def test_sensor(hass): assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" - entry = registry.async_get( - "sensor.nettigo_air_monitor_sds011_particulate_matter_10" - ) + entry = registry.async_get("sensor.nettigo_air_monitor_sds011_caqi_level") assert entry - assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p1" + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_caqi_level" state = hass.states.get("sensor.nettigo_air_monitor_sds011_particulate_matter_2_5") assert state @@ -279,34 +346,34 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds011_p2" - state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0") + state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi") assert state - assert state.state == "31" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - ) + assert state.state == "54" + assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi") assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi" - state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi") + state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi_level") assert state - assert state.state == "54" + assert state.state == "medium" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" entry = registry.async_get("sensor.nettigo_air_monitor_sps30_caqi_level") assert entry assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30_caqi_level" - state = hass.states.get("sensor.nettigo_air_monitor_sps30_caqi_level") + state = hass.states.get("sensor.nettigo_air_monitor_sps30_particulate_matter_1_0") assert state - assert state.state == "medium" - assert state.attributes.get(ATTR_DEVICE_CLASS) == "nam__caqi_level" - assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" + assert state.state == "31" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM1 + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) entry = registry.async_get( "sensor.nettigo_air_monitor_sps30_particulate_matter_1_0" From 927b8b2eef1435c78e710dd509b9703e93b9d6b4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 Oct 2022 11:09:15 +0100 Subject: [PATCH 062/394] Bump pyatmo to 7.3.0 (#81290) * Bump pyatmo to 7.3.0 * Update test fixture data and tests --- .../components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../netatmo/fixtures/getstationsdata.json | 222 ++++-- .../netatmo/fixtures/homesdata.json | 236 +++--- .../homestatus_91763b24c43d3e344f424e8b.json | 696 ++---------------- .../homestatus_91763b24c43d3e344f424e8c.json | 18 +- tests/components/netatmo/test_camera.py | 21 +- tests/components/netatmo/test_climate.py | 18 +- tests/components/netatmo/test_light.py | 12 +- tests/components/netatmo/test_sensor.py | 34 +- 11 files changed, 383 insertions(+), 880 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 8beb7bc521a7d3..436b6329c1da65 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.2.0"], + "requirements": ["pyatmo==7.3.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index 93cee8ab0285de..eedb8fd71d5a05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1445,7 +1445,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae0bbe72346be6..ac8162f39bf126 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1033,7 +1033,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.apple_tv pyatv==0.10.3 diff --git a/tests/components/netatmo/fixtures/getstationsdata.json b/tests/components/netatmo/fixtures/getstationsdata.json index 822a4c11a5016a..10c3ca85e06c2c 100644 --- a/tests/components/netatmo/fixtures/getstationsdata.json +++ b/tests/components/netatmo/fixtures/getstationsdata.json @@ -114,7 +114,7 @@ "battery_percent": 79 }, { - "_id": "12:34:56:03:1b:e4", + "_id": "12:34:56:03:1b:e5", "type": "NAModule2", "module_name": "Garden", "data_type": ["Wind"], @@ -430,63 +430,203 @@ "modules": [] }, { - "_id": "12:34:56:58:c8:54", - "date_setup": 1605594014, - "last_setup": 1605594014, + "_id": "12:34:56:80:bb:26", + "station_name": "MYHOME (Palier)", + "date_setup": 1558709904, + "last_setup": 1558709904, "type": "NAMain", - "last_status_store": 1605878352, - "firmware": 178, - "wifi_status": 47, + "last_status_store": 1644582700, + "module_name": "Palier", + "firmware": 181, + "last_upgrade": 1558709906, + "wifi_status": 57, "reachable": true, "co2_calibrating": false, "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], "place": { - "altitude": 65, - "city": "Njurunda District", - "country": "SE", - "timezone": "Europe/Stockholm", - "location": [17.123456, 62.123456] + "altitude": 329, + "city": "Someplace", + "country": "FR", + "timezone": "Europe/Paris", + "location": [6.1234567, 46.123456] }, - "station_name": "Njurunda (Indoor)", - "home_id": "5fb36b9ec68fd10c6467ca65", - "home_name": "Njurunda", + "home_id": "91763b24c43d3e344f424e8b", + "home_name": "MYHOME", "dashboard_data": { - "time_utc": 1605878349, - "Temperature": 19.7, - "CO2": 993, - "Humidity": 40, - "Noise": 40, - "Pressure": 1015.6, - "AbsolutePressure": 1007.8, - "min_temp": 19.7, - "max_temp": 20.4, - "date_max_temp": 1605826917, - "date_min_temp": 1605873207, + "time_utc": 1644582694, + "Temperature": 21.1, + "CO2": 1339, + "Humidity": 45, + "Noise": 35, + "Pressure": 1026.8, + "AbsolutePressure": 974.5, + "min_temp": 21, + "max_temp": 21.8, + "date_max_temp": 1644534255, + "date_min_temp": 1644550420, "temp_trend": "stable", "pressure_trend": "up" }, "modules": [ { - "_id": "12:34:56:58:e6:38", + "_id": "12:34:56:80:1c:42", "type": "NAModule1", - "last_setup": 1605594034, + "module_name": "Outdoor", + "last_setup": 1558709954, "data_type": ["Temperature", "Humidity"], - "battery_percent": 100, + "battery_percent": 27, "reachable": true, "firmware": 50, - "last_message": 1605878347, - "last_seen": 1605878328, - "rf_status": 62, - "battery_vp": 6198, + "last_message": 1644582699, + "last_seen": 1644582699, + "rf_status": 68, + "battery_vp": 4678, "dashboard_data": { - "time_utc": 1605878328, - "Temperature": 0.6, - "Humidity": 77, - "min_temp": -2.1, - "max_temp": 1.5, - "date_max_temp": 1605865920, - "date_min_temp": 1605826904, - "temp_trend": "down" + "time_utc": 1644582648, + "Temperature": 9.4, + "Humidity": 57, + "min_temp": 6.7, + "max_temp": 9.8, + "date_max_temp": 1644534223, + "date_min_temp": 1644569369, + "temp_trend": "up" + } + }, + { + "_id": "12:34:56:80:c1:ea", + "type": "NAModule3", + "module_name": "Rain", + "last_setup": 1563734531, + "data_type": ["Rain"], + "battery_percent": 21, + "reachable": true, + "firmware": 12, + "last_message": 1644582699, + "last_seen": 1644582699, + "rf_status": 79, + "battery_vp": 4256, + "dashboard_data": { + "time_utc": 1644582686, + "Rain": 3.7, + "sum_rain_1": 0, + "sum_rain_24": 6.9 + } + }, + { + "_id": "12:34:56:80:44:92", + "type": "NAModule4", + "module_name": "Bedroom", + "last_setup": 1575915890, + "data_type": ["Temperature", "CO2", "Humidity"], + "battery_percent": 28, + "reachable": true, + "firmware": 51, + "last_message": 1644582699, + "last_seen": 1644582654, + "rf_status": 67, + "battery_vp": 4695, + "dashboard_data": { + "time_utc": 1644582654, + "Temperature": 19.3, + "CO2": 1076, + "Humidity": 53, + "min_temp": 19.2, + "max_temp": 19.7, + "date_max_temp": 1644534243, + "date_min_temp": 1644553418, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:80:7e:18", + "type": "NAModule4", + "module_name": "Bathroom", + "last_setup": 1575915955, + "data_type": ["Temperature", "CO2", "Humidity"], + "battery_percent": 55, + "reachable": true, + "firmware": 51, + "last_message": 1644582699, + "last_seen": 1644582654, + "rf_status": 59, + "battery_vp": 5184, + "dashboard_data": { + "time_utc": 1644582654, + "Temperature": 19.4, + "CO2": 1930, + "Humidity": 55, + "min_temp": 19.4, + "max_temp": 21.8, + "date_max_temp": 1644534224, + "date_min_temp": 1644582039, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": ["Wind"], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + } + ] + }, + { + "_id": "00:11:22:2c:be:c8", + "station_name": "Zuhause (Kinderzimmer)", + "type": "NAMain", + "last_status_store": 1649146022, + "reachable": true, + "favorite": true, + "data_type": ["Pressure"], + "place": { + "altitude": 127, + "city": "Wiesbaden", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [8.238054275512695, 50.07585525512695] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1649146022, + "Pressure": 1015.6, + "AbsolutePressure": 1000.4, + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "00:11:22:2c:ce:b6", + "type": "NAModule1", + "data_type": ["Temperature", "Humidity"], + "reachable": true, + "last_message": 1649146022, + "last_seen": 1649145996, + "dashboard_data": { + "time_utc": 1649145996, + "Temperature": 7.8, + "Humidity": 87, + "min_temp": 6.5, + "max_temp": 7.8, + "date_max_temp": 1649145996, + "date_min_temp": 1649118465, + "temp_trend": "up" } } ] diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index 93c04388f4c190..6b24a7f8f9d4ac 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -23,7 +23,6 @@ "12:34:56:00:f1:62", "12:34:56:10:f1:66", "12:34:56:00:e3:9b", - "12:34:56:00:86:99", "0009999992" ] }, @@ -39,12 +38,6 @@ "type": "kitchen", "module_ids": ["12:34:56:03:a0:ac"] }, - { - "id": "2940411588", - "name": "Child", - "type": "custom", - "module_ids": ["12:34:56:26:cc:01"] - }, { "id": "222452125", "name": "Bureau", @@ -76,6 +69,12 @@ "name": "Corridor", "type": "corridor", "module_ids": ["10:20:30:bd:b8:1e"] + }, + { + "id": "100007520", + "name": "Toilettes", + "type": "toilets", + "module_ids": ["00:11:22:33:00:11:45:fe"] } ], "modules": [ @@ -120,15 +119,29 @@ "name": "Hall", "setup_date": 1544828430, "room_id": "3688132631", - "reachable": true, "modules_bridged": ["12:34:56:00:86:99", "12:34:56:00:e3:9b"] }, { - "id": "12:34:56:00:a5:a4", + "id": "12:34:56:10:f1:66", + "type": "NDB", + "name": "Netatmo-Doorbell", + "setup_date": 1602691361, + "room_id": "3688132631", + "reachable": true, + "hk_device_id": "123456007df1", + "customer_id": "1000010", + "network_lock": false, + "quick_display_zone": 62 + }, + { + "id": "12:34:56:10:b9:0e", "type": "NOC", - "name": "Garden", - "setup_date": 1544828430, - "reachable": true + "name": "Front", + "setup_date": 1509290599, + "reachable": true, + "customer_id": "A00010", + "network_lock": false, + "use_pincode": false }, { "id": "12:34:56:20:f5:44", @@ -155,33 +168,6 @@ "room_id": "222452125", "bridge": "12:34:56:20:f5:44" }, - { - "id": "12:34:56:10:f1:66", - "type": "NDB", - "name": "Netatmo-Doorbell", - "setup_date": 1602691361, - "room_id": "3688132631", - "reachable": true, - "hk_device_id": "123456007df1", - "customer_id": "1000010", - "network_lock": false, - "quick_display_zone": 62 - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "setup_date": 1620479901, - "bridge": "12:34:56:00:f1:62", - "name": "Sirene in hall" - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "name": "Window Hall", - "setup_date": 1581177375, - "bridge": "12:34:56:00:f1:62", - "category": "window" - }, { "id": "12:34:56:30:d5:d4", "type": "NBG", @@ -199,16 +185,17 @@ "bridge": "12:34:56:30:d5:d4" }, { - "id": "12:34:56:37:11:ca", + "id": "12:34:56:80:bb:26", "type": "NAMain", - "name": "NetatmoIndoor", + "name": "Villa", "setup_date": 1419453350, + "room_id": "4122897288", "reachable": true, "modules_bridged": [ - "12:34:56:07:bb:3e", - "12:34:56:03:1b:e4", - "12:34:56:36:fc:de", - "12:34:56:05:51:20" + "12:34:56:80:44:92", + "12:34:56:80:7e:18", + "12:34:56:80:1c:42", + "12:34:56:80:c1:ea" ], "customer_id": "C00016", "hardware_version": 251, @@ -271,48 +258,46 @@ "module_offset": { "12:34:56:80:bb:26": { "a": 0.1 + }, + "03:00:00:03:1b:0e": { + "a": 0 } } }, { - "id": "12:34:56:36:fc:de", + "id": "12:34:56:80:1c:42", "type": "NAModule1", "name": "Outdoor", "setup_date": 1448565785, - "bridge": "12:34:56:37:11:ca" - }, - { - "id": "12:34:56:03:1b:e4", - "type": "NAModule2", - "name": "Garden", - "setup_date": 1543579864, - "bridge": "12:34:56:37:11:ca" + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:05:51:20", + "id": "12:34:56:80:c1:ea", "type": "NAModule3", "name": "Rain", "setup_date": 1591770206, - "bridge": "12:34:56:37:11:ca" + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:07:bb:3e", + "id": "12:34:56:80:44:92", "type": "NAModule4", "name": "Bedroom", "setup_date": 1484997703, - "bridge": "12:34:56:37:11:ca" + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:26:68:92", - "type": "NHC", - "name": "Indoor", - "setup_date": 1571342643 + "id": "12:34:56:80:7e:18", + "type": "NAModule4", + "name": "Bathroom", + "setup_date": 1543579864, + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:26:cc:01", - "type": "BNS", - "name": "Child", - "setup_date": 1571634243 + "id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "name": "Garden", + "setup_date": 1543579864, + "bridge": "12:34:56:80:bb:26" }, { "id": "12:34:56:80:60:40", @@ -324,7 +309,8 @@ "12:34:56:80:00:12:ac:f2", "12:34:56:80:00:c3:69:3c", "12:34:56:00:00:a1:4c:da", - "12:34:56:00:01:01:01:a1" + "12:34:56:00:01:01:01:a1", + "00:11:22:33:00:11:45:fe" ] }, { @@ -342,6 +328,21 @@ "setup_date": 1641841262, "bridge": "12:34:56:80:60:40" }, + { + "id": "12:34:56:00:86:99", + "type": "NACamDoorTag", + "name": "Window Hall", + "setup_date": 1581177375, + "bridge": "12:34:56:00:f1:62", + "category": "window" + }, + { + "id": "12:34:56:00:e3:9b", + "type": "NIS", + "setup_date": 1620479901, + "bridge": "12:34:56:00:f1:62", + "name": "Sirene in hall" + }, { "id": "12:34:56:00:16:0e", "type": "NLE", @@ -440,6 +441,24 @@ "room_id": "100008999", "bridge": "12:34:56:80:60:40" }, + { + "id": "10:20:30:bd:b8:1e", + "type": "BNS", + "name": "Smarther", + "setup_date": 1638022197, + "room_id": "1002003001" + }, + { + "id": "00:11:22:33:00:11:45:fe", + "type": "NLF", + "on": false, + "brightness": 63, + "firmware_revision": 57, + "last_seen": 1657086939, + "power": 0, + "reachable": true, + "bridge": "12:34:56:80:60:40" + }, { "id": "12:34:56:00:01:01:01:a1", "type": "NLFN", @@ -761,80 +780,13 @@ "therm_mode": "schedule" }, { - "id": "111111111111111111111401", - "name": "Home with no modules", - "altitude": 9, - "coordinates": [1.23456789, 50.0987654], - "country": "BE", - "timezone": "Europe/Brussels", - "rooms": [ - { - "id": "1111111401", - "name": "Livingroom", - "type": "livingroom" - } - ], - "temperature_control_mode": "heating", - "therm_mode": "away", - "therm_setpoint_default_duration": 120, - "cooling_mode": "schedule", - "schedules": [ - { - "away_temp": 14, - "hg_temp": 7, - "name": "Week", - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 6, - "m_offset": 420 - } - ], - "zones": [ - { - "type": 0, - "name": "Comfort", - "rooms_temp": [], - "id": 0, - "rooms": [] - }, - { - "type": 1, - "name": "Nacht", - "rooms_temp": [], - "id": 1, - "rooms": [] - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [], - "id": 4, - "rooms": [] - }, - { - "type": 4, - "name": "Tussenin", - "rooms_temp": [], - "id": 5, - "rooms": [] - }, - { - "type": 4, - "name": "Ochtend", - "rooms_temp": [], - "id": 6, - "rooms": [] - } - ], - "id": "700000000000000000000401", - "selected": true, - "type": "therm" - } - ] + "id": "91763b24c43d3e344f424e8c", + "altitude": 112, + "coordinates": [52.516263, 13.377726], + "country": "DE", + "timezone": "Europe/Berlin", + "therm_setpoint_default_duration": 180, + "therm_mode": "schedule" } ], "user": { @@ -845,6 +797,8 @@ "unit_pressure": 0, "unit_system": 0, "unit_wind": 0, + "all_linked": false, + "type": "netatmo", "id": "91763b24c43d3e344f424e8b" } }, diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json index 4cd5dceec3bbb7..736d70be11cde3 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json @@ -14,25 +14,6 @@ "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,", "is_local": true }, - { - "type": "NOC", - "firmware_revision": 3002000, - "monitoring": "on", - "sd_status": 4, - "connection": "wifi", - "homekit_status": "upgradable", - "floodlight": "auto", - "timelapse_available": true, - "id": "12:34:56:00:a5:a4", - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,", - "is_local": false, - "network_lock": false, - "firmware_name": "3.2.0", - "wifi_strength": 62, - "alim_status": 2, - "locked": false, - "wifi_state": "high" - }, { "id": "12:34:56:00:fa:d0", "type": "NAPlug", @@ -46,6 +27,7 @@ "type": "NATherm1", "firmware_revision": 65, "rf_strength": 58, + "battery_level": 3793, "boiler_valve_comfort_boost": false, "boiler_status": false, "anticipating": false, @@ -58,6 +40,7 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 51, + "battery_level": 3025, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, @@ -67,18 +50,10 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 59, + "battery_level": 3029, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, - { - "id": "12:34:56:26:cc:01", - "type": "BNS", - "firmware_revision": 32, - "wifi_strength": 50, - "boiler_valve_comfort_boost": false, - "boiler_status": true, - "cooler_status": false - }, { "type": "NDB", "last_ftp_event": { @@ -100,6 +75,25 @@ "wifi_strength": 66, "wifi_state": "medium" }, + { + "type": "NOC", + "firmware_revision": 3002000, + "monitoring": "on", + "sd_status": 4, + "connection": "wifi", + "homekit_status": "upgradable", + "floodlight": "auto", + "timelapse_available": true, + "id": "12:34:56:10:b9:0e", + "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,", + "is_local": false, + "network_lock": false, + "firmware_name": "3.2.0", + "wifi_strength": 62, + "alim_status": 2, + "locked": false, + "wifi_state": "high" + }, { "boiler_control": "onoff", "dhw_control": "none", @@ -264,629 +258,43 @@ "bridge": "12:34:56:80:60:40" }, { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, - "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, + "id": "10:20:30:bd:b8:1e", + "type": "BNS", + "firmware_revision": 32, + "wifi_strength": 49, "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true + "boiler_status": true, + "cooler_status": false }, { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, + "id": "00:11:22:33:00:11:45:fe", + "type": "NLF", + "on": false, + "brightness": 63, + "firmware_revision": 57, + "last_seen": 1657086939, "power": 0, "reachable": true, "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, + } + ], + "rooms": [ { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, + "id": "2746182631", "reachable": true, - "bridge": "12:34:56:80:60:40" + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0 }, { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, + "id": "2940411577", "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, - "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true - }, - { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, - "power": 0, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, - "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, - "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true - }, - { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, - "power": 0, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, - "reachable": true, - "bridge": "12:34:56:80:60:40" - } - ], - "rooms": [ - { - "id": "2746182631", - "reachable": true, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "schedule", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0 - }, - { - "id": "2940411577", - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, + "therm_measured_temperature": 27, + "heating_power_request": 0, "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", + "therm_setpoint_mode": "hg", "therm_setpoint_start_time": 0, "therm_setpoint_end_time": 0, "anticipating": false, @@ -905,15 +313,15 @@ "open_window": false }, { - "id": "2940411588", + "id": "1002003001", "reachable": true, "anticipating": false, "heating_power_request": 0, "open_window": false, - "humidity": 68, - "therm_measured_temperature": 19.9, - "therm_setpoint_temperature": 21.5, - "therm_setpoint_start_time": 1647793285, + "humidity": 67, + "therm_measured_temperature": 22, + "therm_setpoint_temperature": 22, + "therm_setpoint_start_time": 1647462737, "therm_setpoint_end_time": null, "therm_setpoint_mode": "home" } diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json index d950c82a6a5ced..406e24bc1077dc 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json @@ -1,12 +1,20 @@ { "status": "ok", - "time_server": 1559292041, + "time_server": 1642952130, "body": { "home": { - "modules": [], - "rooms": [], - "id": "91763b24c43d3e344f424e8c", - "persons": [] + "persons": [ + { + "id": "abcdef12-1111-0000-0000-000111222333", + "last_seen": 1489050910, + "out_of_sight": true + }, + { + "id": "abcdef12-2222-0000-0000-000111222333", + "last_seen": 1489078776, + "out_of_sight": true + } + ] } } } diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index beb91c7565e2d3..76397988187b49 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -33,7 +33,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): await hass.async_block_till_done() camera_entity_indoor = "camera.hall" - camera_entity_outdoor = "camera.garden" + camera_entity_outdoor = "camera.front" assert hass.states.get(camera_entity_indoor).state == "streaming" response = { "event_type": "off", @@ -59,8 +59,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "on", @@ -72,8 +72,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "auto", @@ -84,7 +84,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", } @@ -166,7 +166,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) uri = "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,," stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.garden" + camera_entity_indoor = "camera.front" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -304,14 +304,14 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.garden", + "entity_id": "camera.front", "camera_light_mode": "on", } expected_data = { "modules": [ { - "id": "12:34:56:00:a5:a4", + "id": "12:34:56:10:b9:0e", "floodlight": "on", }, ], @@ -353,7 +353,6 @@ async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo assert excinfo.value.args == ("NACamera does not have a floodlight",) -@pytest.mark.skip async def test_camera_reconnect_webhook(hass, config_entry): """Test webhook event on camera reconnect.""" fake_post_hits = 0 @@ -406,7 +405,7 @@ async def fake_post(*args, **kwargs): dt.utcnow() + timedelta(seconds=60), ) await hass.async_block_till_done() - assert fake_post_hits > calls + assert fake_post_hits >= calls async def test_webhook_person_event(hass, config_entry, netatmo_auth): diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index d37bab929e1518..afe85049f95d8d 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -36,8 +36,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 12 @@ -80,8 +79,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "heat" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 21 @@ -194,8 +192,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) @@ -213,8 +210,7 @@ async def test_service_preset_mode_frost_guard_thermostat( assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) # Test service setting the preset mode to "frost guard" @@ -269,8 +265,7 @@ async def test_service_preset_mode_frost_guard_thermostat( assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) @@ -286,8 +281,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth) assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) # Test service setting the preset mode to "away" diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index b1a5270745ce87..526fb2fe518b48 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -27,14 +27,14 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await hass.async_block_till_done() - light_entity = "light.garden" + light_entity = "light.front" assert hass.states.get(light_entity).state == "unavailable" # Trigger light mode change response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "on", @@ -46,7 +46,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) # Trigger light mode change with erroneous webhook data response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", } await simulate_webhook(hass, webhook_id, response) @@ -62,7 +62,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "auto"}]} + {"modules": [{"id": "12:34:56:10:b9:0e", "floodlight": "auto"}]} ) # Test turning light on @@ -75,7 +75,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "on"}]} + {"modules": [{"id": "12:34:56:10:b9:0e", "floodlight": "on"}]} ) diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index d3ea8fb8167a65..9ef5637231615c 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -16,12 +16,12 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): await hass.async_block_till_done() - prefix = "sensor.netatmoindoor_" + prefix = "sensor.parents_bedroom_" - assert hass.states.get(f"{prefix}temperature").state == "24.6" - assert hass.states.get(f"{prefix}humidity").state == "36" - assert hass.states.get(f"{prefix}co2").state == "749" - assert hass.states.get(f"{prefix}pressure").state == "1017.3" + assert hass.states.get(f"{prefix}temperature").state == "20.3" + assert hass.states.get(f"{prefix}humidity").state == "63" + assert hass.states.get(f"{prefix}co2").state == "494" + assert hass.states.get(f"{prefix}pressure").state == "1014.5" async def test_public_weather_sensor(hass, config_entry, netatmo_auth): @@ -104,25 +104,25 @@ async def test_process_health(health, expected): @pytest.mark.parametrize( "uid, name, expected", [ - ("12:34:56:37:11:ca-reachable", "mystation_reachable", "True"), - ("12:34:56:03:1b:e4-rf_status", "mystation_yard_radio", "Full"), + ("12:34:56:03:1b:e4-reachable", "villa_garden_reachable", "True"), + ("12:34:56:03:1b:e4-rf_status", "villa_garden_radio", "Full"), ( - "12:34:56:37:11:ca-wifi_status", - "mystation_wifi_strength", - "Full", + "12:34:56:80:bb:26-wifi_status", + "villa_wifi_strength", + "High", ), ( - "12:34:56:37:11:ca-temp_trend", - "mystation_temperature_trend", + "12:34:56:80:bb:26-temp_trend", + "villa_temperature_trend", "stable", ), ( - "12:34:56:37:11:ca-pressure_trend", - "netatmo_mystation_pressure_trend", - "down", + "12:34:56:80:bb:26-pressure_trend", + "villa_pressure_trend", + "up", ), - ("12:34:56:05:51:20-sum_rain_1", "netatmo_mystation_yard_rain_last_hour", "0"), - ("12:34:56:05:51:20-sum_rain_24", "netatmo_mystation_yard_rain_today", "0"), + ("12:34:56:80:c1:ea-sum_rain_1", "villa_rain_rain_last_hour", "0"), + ("12:34:56:80:c1:ea-sum_rain_24", "villa_rain_rain_today", "6.9"), ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"), ( "12:34:56:03:1b:e4-windangle_value", From dd092d86d47c9a78129ee9e31f03062ec03c9340 Mon Sep 17 00:00:00 2001 From: Josh Anderson Date: Mon, 31 Oct 2022 12:19:52 +0000 Subject: [PATCH 063/394] Update supported and deprecated IBM Watson voices (#81247) --- homeassistant/components/watson_tts/tts.py | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 93e6b98fbd6287..efd20e37e83b23 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -23,6 +23,7 @@ SUPPORTED_VOICES = [ "ar-AR_OmarVoice", "ar-MS_OmarVoice", + "cs-CZ_AlenaVoice", "de-DE_BirgitV2Voice", "de-DE_BirgitV3Voice", "de-DE_BirgitVoice", @@ -32,21 +33,26 @@ "de-DE_ErikaV3Voice", "en-AU_CraigVoice", "en-AU_MadisonVoice", + "en-AU_SteveVoice", "en-GB_KateV3Voice", "en-GB_KateVoice", "en-GB_CharlotteV3Voice", "en-GB_JamesV3Voice", "en-GB_KateV3Voice", "en-GB_KateVoice", + "en-US_AllisonExpressive", "en-US_AllisonV2Voice", "en-US_AllisonV3Voice", "en-US_AllisonVoice", "en-US_EmilyV3Voice", + "en-US_EmmaExpressive", "en-US_HenryV3Voice", "en-US_KevinV3Voice", + "en-US_LisaExpressive", "en-US_LisaV2Voice", "en-US_LisaV3Voice", "en-US_LisaVoice", + "en-US_MichaelExpressive", "en-US_MichaelV2Voice", "en-US_MichaelV3Voice", "en-US_MichaelVoice", @@ -72,10 +78,13 @@ "ko-KR_SiWooVoice", "ko-KR_YoungmiVoice", "ko-KR_YunaVoice", + "nl-BE_AdeleVoice", + "nl-BE_BramVoice", "nl-NL_EmmaVoice", "nl-NL_LiamVoice", "pt-BR_IsabelaV3Voice", "pt-BR_IsabelaVoice", + "sv-SE_IngridVoice", "zh-CN_LiNaVoice", "zh-CN_WangWeiVoice", "zh-CN_ZhangJingVoice", @@ -83,8 +92,13 @@ DEPRECATED_VOICES = [ "ar-AR_OmarVoice", + "ar-MS_OmarVoice", + "cs-CZ_AlenaVoice", "de-DE_BirgitVoice", "de-DE_DieterVoice", + "en-AU_CraigVoice", + "en-AU_MadisonVoice", + "en-AU_SteveVoice", "en-GB_KateVoice", "en-GB_KateV3Voice", "en-US_AllisonVoice", @@ -97,7 +111,19 @@ "fr-FR_ReneeVoice", "it-IT_FrancescaVoice", "ja-JP_EmiVoice", + "ko-KR_HyunjunVoice", + "ko-KR_SiWooVoice", + "ko-KR_YoungmiVoice", + "ko-KR_YunaVoice", + "nl-BE_AdeleVoice", + "nl-BE_BramVoice", + "nl-NL_EmmaVoice", + "nl-NL_LiamVoice", "pt-BR_IsabelaVoice", + "sv-SE_IngridVoice", + "zh-CN_LiNaVoice", + "zh-CN_WangWeiVoice", + "zh-CN_ZhangJingVoice", ] SUPPORTED_OUTPUT_FORMATS = [ From 283f8585b89d274ab2dbd9b9a7d54d0e2b0b5c58 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Oct 2022 13:21:37 +0100 Subject: [PATCH 064/394] Adjust scrape coordinator logging (#81299) --- homeassistant/components/scrape/coordinator.py | 4 +++- homeassistant/components/scrape/sensor.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scrape/coordinator.py b/homeassistant/components/scrape/coordinator.py index d947e6ac519873..9fc66db34810c4 100644 --- a/homeassistant/components/scrape/coordinator.py +++ b/homeassistant/components/scrape/coordinator.py @@ -33,4 +33,6 @@ async def _async_update_data(self) -> BeautifulSoup: await self._rest.async_update() if (data := self._rest.data) is None: raise UpdateFailed("REST data is not available") - return await self.hass.async_add_executor_job(BeautifulSoup, data, "lxml") + soup = await self.hass.async_add_executor_job(BeautifulSoup, data, "lxml") + _LOGGER.debug("Raw beautiful soup: %s", soup) + return soup diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index d6e5a60d339a89..176b556e1899c9 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -170,7 +170,6 @@ def __init__( def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" raw_data = self.coordinator.data - _LOGGER.debug("Raw beautiful soup: %s", raw_data) try: if self._attr is not None: value = raw_data.select(self._select)[self._index][self._attr] From be68412c6490fd0c6e3ab3c922431e205650e349 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Oct 2022 13:26:05 +0100 Subject: [PATCH 065/394] Update pytest to 7.2.0 (#81295) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b15ceb3b0023ca..06bd7e878ce2c1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -24,7 +24,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-xdist==2.5.0 -pytest==7.1.3 +pytest==7.2.0 requests_mock==1.10.0 respx==0.19.2 stdlib-list==0.7.0 From fbc8f0a2cf6b914d3d8a6c6b6d43db39e05bde90 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:06:09 +0100 Subject: [PATCH 066/394] Improve type hints in rest integration (#81291) --- homeassistant/components/rest/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rest/__init__.py b/homeassistant/components/rest/__init__.py index c9efbc30d4acc4..4ec519dd709980 100644 --- a/homeassistant/components/rest/__init__.py +++ b/homeassistant/components/rest/__init__.py @@ -2,9 +2,11 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine import contextlib from datetime import timedelta import logging +from typing import Any import httpx import voluptuous as vol @@ -88,15 +90,15 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool if DOMAIN not in config: return True - refresh_tasks = [] - load_tasks = [] + refresh_coroutines: list[Coroutine[Any, Any, None]] = [] + load_coroutines: list[Coroutine[Any, Any, None]] = [] rest_config: list[ConfigType] = config[DOMAIN] for rest_idx, conf in enumerate(rest_config): scan_interval: timedelta = conf.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) resource_template: template.Template | None = conf.get(CONF_RESOURCE_TEMPLATE) rest = create_rest_data_from_config(hass, conf) coordinator = _rest_coordinator(hass, rest, resource_template, scan_interval) - refresh_tasks.append(coordinator.async_refresh()) + refresh_coroutines.append(coordinator.async_refresh()) hass.data[DOMAIN][REST_DATA].append({REST: rest, COORDINATOR: coordinator}) for platform_domain in COORDINATOR_AWARE_PLATFORMS: @@ -107,20 +109,20 @@ async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> bool hass.data[DOMAIN][platform_domain].append(platform_conf) platform_idx = len(hass.data[DOMAIN][platform_domain]) - 1 - load = discovery.async_load_platform( + load_coroutine = discovery.async_load_platform( hass, platform_domain, DOMAIN, {REST_IDX: rest_idx, PLATFORM_IDX: platform_idx}, config, ) - load_tasks.append(load) + load_coroutines.append(load_coroutine) - if refresh_tasks: - await asyncio.gather(*refresh_tasks) + if refresh_coroutines: + await asyncio.gather(*refresh_coroutines) - if load_tasks: - await asyncio.gather(*load_tasks) + if load_coroutines: + await asyncio.gather(*load_coroutines) return True From 5ba3b499fe77f47e3385112238c02fa830bd8f79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:18:49 -0500 Subject: [PATCH 067/394] Bump dbus-fast to 1.60.0 (#81296) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 091962fbc83294..bca2f7f9a8d1c7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.1", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.59.1" + "dbus-fast==1.60.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 914731a81649c6..e2c57d87c19c03 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.59.1 +dbus-fast==1.60.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index eedb8fd71d5a05..f3db2448dd06dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.1 +dbus-fast==1.60.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac8162f39bf126..45b8e84298a9cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.1 +dbus-fast==1.60.0 # homeassistant.components.debugpy debugpy==1.6.3 From 8416cc1906daf184bb5416759ebee52b108992c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:27:04 -0500 Subject: [PATCH 068/394] Try to switch to a different esphome BLE proxy if we run out of slots while connecting (#81268) --- homeassistant/components/bluetooth/models.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a2e50fe1182256..a63a704baf6c7b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -264,6 +264,7 @@ def __init__( # pylint: disable=super-init-not-called, keyword-arg-before-varar self.__address = address_or_ble_device self.__disconnected_callback = disconnected_callback self.__timeout = timeout + self.__ble_device: BLEDevice | None = None self._backend: BaseBleakClient | None = None # type: ignore[assignment] @property @@ -283,14 +284,21 @@ def set_disconnected_callback( async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" - if not self._backend: + if ( + not self._backend + or not self.__ble_device + or not self._async_get_backend_for_ble_device(self.__ble_device) + ): assert MANAGER is not None wrapped_backend = ( self._async_get_backend() or self._async_get_fallback_backend() ) - self._backend = wrapped_backend.client( + self.__ble_device = ( await freshen_ble_device(wrapped_backend.device) - or wrapped_backend.device, + or wrapped_backend.device + ) + self._backend = wrapped_backend.client( + self.__ble_device, disconnected_callback=self.__disconnected_callback, timeout=self.__timeout, hass=MANAGER.hass, From 1589c06203c0bc9f87adcc97fe34d5c52aaf403a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:35:08 -0500 Subject: [PATCH 069/394] Significantly reduce clock_gettime syscalls on platforms with broken vdso (#81257) --- .../bluetooth/active_update_coordinator.py | 6 ++--- homeassistant/components/bluetooth/manager.py | 4 +-- homeassistant/components/bluetooth/scanner.py | 4 +-- homeassistant/components/bluetooth/util.py | 4 +-- .../components/esphome/bluetooth/scanner.py | 3 ++- homeassistant/util/dt.py | 26 +++++++++++++++++++ tests/util/test_dt.py | 6 +++++ 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 37f049d3e0724e..ab26a0260f38ac 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -3,13 +3,13 @@ from collections.abc import Callable, Coroutine import logging -import time from typing import Any, Generic, TypeVar from bleak import BleakError from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.util.dt import monotonic_time_coarse from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .passive_update_processor import PassiveBluetoothProcessorCoordinator @@ -94,7 +94,7 @@ def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool: """Return true if time to try and poll.""" poll_age: float | None = None if self._last_poll: - poll_age = time.monotonic() - self._last_poll + poll_age = monotonic_time_coarse() - self._last_poll return self._needs_poll_method(service_info, poll_age) async def _async_poll_data( @@ -124,7 +124,7 @@ async def _async_poll(self) -> None: self.last_poll_successful = False return finally: - self._last_poll = time.monotonic() + self._last_poll = monotonic_time_coarse() if not self.last_poll_successful: self.logger.debug("%s: Polling recovered") diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index aaefd3dcfc4c44..c3a0e0998f1919 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta import itertools import logging -import time from typing import TYPE_CHECKING, Any, Final from bleak.backends.scanner import AdvertisementDataCallback @@ -22,6 +21,7 @@ ) from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker from .const import ( @@ -69,7 +69,7 @@ APPLE_DEVICE_ID_START_BYTE, } -MONOTONIC_TIME: Final = time.monotonic +MONOTONIC_TIME: Final = monotonic_time_coarse _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index fe795f7ace556f..6b23cae02183e7 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -6,7 +6,6 @@ from datetime import datetime import logging import platform -import time from typing import Any import async_timeout @@ -22,6 +21,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from homeassistant.util.package import is_docker_env from .const import ( @@ -35,7 +35,7 @@ from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner -MONOTONIC_TIME = time.monotonic +MONOTONIC_TIME = monotonic_time_coarse # or_patterns is a workaround for the fact that passive scanning # needs at least one matcher to be set. The below matcher diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 860428a6106efb..181796d3d2d300 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,11 +2,11 @@ from __future__ import annotations import platform -import time from bluetooth_auto_recovery import recover_adapter from homeassistant.core import callback +from homeassistant.util.dt import monotonic_time_coarse from .const import ( DEFAULT_ADAPTER_BY_PLATFORM, @@ -29,7 +29,7 @@ async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBlea bluez_dbus = BlueZDBusObjects() await bluez_dbus.load() - now = time.monotonic() + now = monotonic_time_coarse() return { address: BluetoothServiceInfoBleak( name=history.advertisement_data.local_name diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 284e605fdfa5fd..7c8064d5583d04 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -19,6 +19,7 @@ ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse TWO_CHAR = re.compile("..") @@ -84,7 +85,7 @@ def discovered_devices_and_advertisement_data( @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" - now = time.monotonic() + now = monotonic_time_coarse() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper name = adv.name if prev_discovery := self._discovered_device_advertisement_datas.get(address): diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 80b322c1a14c8f..44e4403d689dd5 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,7 +4,9 @@ import bisect from contextlib import suppress import datetime as dt +import platform import re +import time from typing import Any import zoneinfo @@ -13,6 +15,7 @@ DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc +CLOCK_MONOTONIC_COARSE = 6 # EPOCHORDINAL is not exposed as a constant # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 @@ -461,3 +464,26 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: assert dattim.tzinfo is not None opposite_fold = dattim.replace(fold=not dattim.fold) return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() + + +def __monotonic_time_coarse() -> float: + """Return a monotonic time in seconds. + + This is the coarse version of time_monotonic, which is faster but less accurate. + + Since many arm64 and 32-bit platforms don't support VDSO with time.monotonic + because of errata, we can't rely on the kernel to provide a fast + monotonic time. + + https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ + """ + return 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 + ): + monotonic_time_coarse = __monotonic_time_coarse diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 79cd4e5e0dfdea..e902176bb35471 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta +import time import pytest @@ -719,3 +720,8 @@ def test_find_next_time_expression_tenth_second_pattern_does_not_drift_entering_ assert (next_target - prev_target).total_seconds() == 60 assert next_target.second == 10 prev_target = next_target + + +def test_monotonic_time_coarse(): + """Test monotonic time coarse.""" + assert abs(time.monotonic() - dt_util.monotonic_time_coarse()) < 1 From 0465510ed7c2e3457d171b8fc023f28887bc7a94 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Sun, 30 Oct 2022 01:23:46 -0400 Subject: [PATCH 070/394] Fix Squeezebox media browsing (#81197) * Squeezebox media browser fix icons * Update pysqueezebox to 0.6.1 --- homeassistant/components/squeezebox/browse_media.py | 1 - homeassistant/components/squeezebox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/browse_media.py b/homeassistant/components/squeezebox/browse_media.py index 979b4c36a98095..c66bc8af9a55ea 100644 --- a/homeassistant/components/squeezebox/browse_media.py +++ b/homeassistant/components/squeezebox/browse_media.py @@ -156,7 +156,6 @@ async def library_payload(hass, player): media_content_type=item, can_play=True, can_expand=True, - thumbnail="https://brands.home-assistant.io/_/squeezebox/logo.png", ) ) diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 018333d420bc27..2c1692b6085627 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -3,7 +3,7 @@ "name": "Squeezebox (Logitech Media Server)", "documentation": "https://www.home-assistant.io/integrations/squeezebox", "codeowners": ["@rajlaud"], - "requirements": ["pysqueezebox==0.6.0"], + "requirements": ["pysqueezebox==0.6.1"], "config_flow": true, "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a4469cfef3be18..410dde0b5e5768 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1920,7 +1920,7 @@ pysoma==0.0.10 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.6.0 +pysqueezebox==0.6.1 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0388c86ed9540..5dcb1bf40ab887 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1355,7 +1355,7 @@ pysoma==0.0.10 pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.6.0 +pysqueezebox==0.6.1 # homeassistant.components.switchbee pyswitchbee==1.5.5 From 8d3ed60986bcff4f89aa6db77460bea5c3ef93c4 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 29 Oct 2022 23:51:53 +0200 Subject: [PATCH 071/394] Fix Danfoss thermostat support in devolo Home Control (#81200) Fix Danfoss thermostat --- homeassistant/components/devolo_home_control/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 95e0628d534af3..6c566aa45e30eb 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -35,6 +35,7 @@ async def async_setup_entry( "devolo.model.Thermostat:Valve", "devolo.model.Room:Thermostat", "devolo.model.Eurotronic:Spirit:Device", + "unk.model.Danfoss:Thermostat", ): entities.append( DevoloClimateDeviceEntity( From be138adb2336418d49423dacdbf098edae68e4ec Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 29 Oct 2022 23:51:11 +0200 Subject: [PATCH 072/394] Add missing string for option traffic_mode for google_travel_time (#81213) Add missing string for option traffic_mode --- homeassistant/components/google_travel_time/strings.json | 1 + homeassistant/components/google_travel_time/translations/en.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/google_travel_time/strings.json b/homeassistant/components/google_travel_time/strings.json index 22a122b9a5350e..78b84038c7fa01 100644 --- a/homeassistant/components/google_travel_time/strings.json +++ b/homeassistant/components/google_travel_time/strings.json @@ -30,6 +30,7 @@ "time_type": "Time Type", "time": "Time", "avoid": "Avoid", + "traffic_mode": "Traffic Mode", "transit_mode": "Transit Mode", "transit_routing_preference": "Transit Routing Preference", "units": "Units" diff --git a/homeassistant/components/google_travel_time/translations/en.json b/homeassistant/components/google_travel_time/translations/en.json index 8e91fbf1df0505..dd03dca1d2f8d5 100644 --- a/homeassistant/components/google_travel_time/translations/en.json +++ b/homeassistant/components/google_travel_time/translations/en.json @@ -28,6 +28,7 @@ "mode": "Travel Mode", "time": "Time", "time_type": "Time Type", + "traffic_mode": "Traffic Mode", "transit_mode": "Transit Mode", "transit_routing_preference": "Transit Routing Preference", "units": "Units" From 24b3d218153fe6c692384609dedc34342237a47f Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 30 Oct 2022 00:04:01 +0200 Subject: [PATCH 073/394] Mute superfluous exception when no Netatmo webhook is to be dropped (#81221) * Mute superfluous exception when no webhook is to be droped * Update homeassistant/components/netatmo/__init__.py Co-authored-by: Paulus Schoutsen --- homeassistant/components/netatmo/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index eb0e93c4b3829b..aa8728d548d05e 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -271,7 +271,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if CONF_WEBHOOK_ID in entry.data: webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await data[entry.entry_id][AUTH].async_dropwebhook() + try: + await data[entry.entry_id][AUTH].async_dropwebhook() + except pyatmo.ApiError: + _LOGGER.debug("No webhook to be dropped") _LOGGER.info("Unregister Netatmo webhook") unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) From a6bb7a083201664ab3d6514265305f2199aa533e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 05:32:57 -0500 Subject: [PATCH 074/394] Bump dbus-fast to 1.59.1 (#81229) * Bump dbus-fast to 1.59.1 fixes incorrect logging of an exception when it was already handled changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.59.0...v1.59.1 * empty --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a5ea8c171d8d69..f8d1867035d376 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.5.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.59.0" + "dbus-fast==1.59.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 413a86be041d85..d48b85e346b80c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.59.0 +dbus-fast==1.59.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 410dde0b5e5768..4914538c5755f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.0 +dbus-fast==1.59.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dcb1bf40ab887..e2f3f0b8685579 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.0 +dbus-fast==1.59.1 # homeassistant.components.debugpy debugpy==1.6.3 From 11bdddc1dc983e75b453f07851c11b05e90c7576 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 30 Oct 2022 20:01:10 +0100 Subject: [PATCH 075/394] Catch `ApiError` while checking credentials in NAM integration (#81243) * Catch ApiError while checking credentials * Update tests * Suggested change --- homeassistant/components/nam/__init__.py | 2 ++ tests/components/nam/test_init.py | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 25615db6eede6e..0fbc93846345c2 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -56,6 +56,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await nam.async_check_credentials() + except ApiError as err: + raise ConfigEntryNotReady from err except AuthFailed as err: raise ConfigEntryAuthFailed from err diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index b6f278d4e94846..a6d11305599204 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -32,12 +32,30 @@ async def test_config_not_ready(hass): unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) + entry.add_to_hass(hass) with patch( "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=ApiError("API Error"), ): - entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_config_not_ready_while_checking_credentials(hass): + """Test for setup failure if the connection fails while checking credentials.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + side_effect=ApiError("API Error"), + ): await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_RETRY @@ -50,12 +68,12 @@ async def test_config_auth_failed(hass): unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) + entry.add_to_hass(hass) with patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailed("Authorization has failed"), ): - entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) assert entry.state is ConfigEntryState.SETUP_ERROR From 90a36894896fbfd5db4023871d4d2e623b7149c2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 30 Oct 2022 13:27:42 +0100 Subject: [PATCH 076/394] Make Netatmo/Legrande/BTicino lights and switches optimistic (#81246) * Make Netatmo lights optimistic * Same for switches --- homeassistant/components/netatmo/light.py | 7 +++++-- homeassistant/components/netatmo/switch.py | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index b3e352eb7d8683..e3bd8952b555be 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -193,17 +193,20 @@ def is_on(self) -> bool: async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" - _LOGGER.debug("Turn light '%s' on", self.name) if ATTR_BRIGHTNESS in kwargs: await self._dimmer.async_set_brightness(kwargs[ATTR_BRIGHTNESS]) else: await self._dimmer.async_on() + self._attr_is_on = True + self.async_write_ha_state() + async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" - _LOGGER.debug("Turn light '%s' off", self.name) await self._dimmer.async_off() + self._attr_is_on = False + self.async_write_ha_state() @callback def async_update_callback(self) -> None: diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py index 338d073c205318..a2e2e67db395f8 100644 --- a/homeassistant/components/netatmo/switch.py +++ b/homeassistant/components/netatmo/switch.py @@ -77,7 +77,11 @@ def async_update_callback(self) -> None: async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self._switch.async_on() + self._attr_is_on = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._switch.async_off() + self._attr_is_on = False + self.async_write_ha_state() From 9d88c953147ef24877b0aff6a2aae9a2eb649a81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 10:35:39 -0500 Subject: [PATCH 077/394] Bump aiohomekit to 2.2.9 (#81254) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 224b24f6077c67..58e258294a02ec 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.8"], + "requirements": ["aiohomekit==2.2.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4914538c5755f1..b3b5a00e8b5474 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.8 +aiohomekit==2.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2f3f0b8685579..54ae8f373e3d49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.8 +aiohomekit==2.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 5f81f968ee3761f3fcc28cb95c27111e225a0b36 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 30 Oct 2022 15:32:19 +0000 Subject: [PATCH 078/394] Set the correct state class for Eve Energy in homekit_controller (#81255) --- homeassistant/components/homekit_controller/sensor.py | 2 +- .../homekit_controller/specific_devices/test_eve_energy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 150f2badc6b6c6..49047b28eaef0e 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -183,7 +183,7 @@ def thread_status_to_str(char: Characteristic) -> str: key=CharacteristicsTypes.VENDOR_EVE_ENERGY_KW_HOUR, name="Energy kWh", device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), CharacteristicsTypes.VENDOR_EVE_ENERGY_VOLTAGE: HomeKitSensorEntityDescription( diff --git a/tests/components/homekit_controller/specific_devices/test_eve_energy.py b/tests/components/homekit_controller/specific_devices/test_eve_energy.py index 65e5c16179f4f7..e678b3bbbaaf23 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_energy.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_energy.py @@ -70,7 +70,7 @@ async def test_eve_energy_setup(hass): entity_id="sensor.eve_energy_50ff_energy_kwh", unique_id="00:00:00:00:00:00_1_28_35", friendly_name="Eve Energy 50FF Energy kWh", - capabilities={"state_class": SensorStateClass.MEASUREMENT}, + capabilities={"state_class": SensorStateClass.TOTAL_INCREASING}, unit_of_measurement=ENERGY_KILO_WATT_HOUR, state="0.28999999165535", ), From 0af69a1014e28ed582a65a1b6faae98d7680422d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:35:08 -0500 Subject: [PATCH 079/394] Significantly reduce clock_gettime syscalls on platforms with broken vdso (#81257) --- .../bluetooth/active_update_coordinator.py | 6 ++--- homeassistant/components/bluetooth/manager.py | 4 +-- homeassistant/components/bluetooth/scanner.py | 4 +-- homeassistant/components/bluetooth/util.py | 4 +-- .../components/esphome/bluetooth/scanner.py | 3 ++- homeassistant/util/dt.py | 26 +++++++++++++++++++ tests/util/test_dt.py | 6 +++++ 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 37f049d3e0724e..ab26a0260f38ac 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -3,13 +3,13 @@ from collections.abc import Callable, Coroutine import logging -import time from typing import Any, Generic, TypeVar from bleak import BleakError from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.util.dt import monotonic_time_coarse from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .passive_update_processor import PassiveBluetoothProcessorCoordinator @@ -94,7 +94,7 @@ def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool: """Return true if time to try and poll.""" poll_age: float | None = None if self._last_poll: - poll_age = time.monotonic() - self._last_poll + poll_age = monotonic_time_coarse() - self._last_poll return self._needs_poll_method(service_info, poll_age) async def _async_poll_data( @@ -124,7 +124,7 @@ async def _async_poll(self) -> None: self.last_poll_successful = False return finally: - self._last_poll = time.monotonic() + self._last_poll = monotonic_time_coarse() if not self.last_poll_successful: self.logger.debug("%s: Polling recovered") diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index aaefd3dcfc4c44..c3a0e0998f1919 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta import itertools import logging -import time from typing import TYPE_CHECKING, Any, Final from bleak.backends.scanner import AdvertisementDataCallback @@ -22,6 +21,7 @@ ) from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker from .const import ( @@ -69,7 +69,7 @@ APPLE_DEVICE_ID_START_BYTE, } -MONOTONIC_TIME: Final = time.monotonic +MONOTONIC_TIME: Final = monotonic_time_coarse _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index fe795f7ace556f..6b23cae02183e7 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -6,7 +6,6 @@ from datetime import datetime import logging import platform -import time from typing import Any import async_timeout @@ -22,6 +21,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from homeassistant.util.package import is_docker_env from .const import ( @@ -35,7 +35,7 @@ from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner -MONOTONIC_TIME = time.monotonic +MONOTONIC_TIME = monotonic_time_coarse # or_patterns is a workaround for the fact that passive scanning # needs at least one matcher to be set. The below matcher diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 860428a6106efb..181796d3d2d300 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,11 +2,11 @@ from __future__ import annotations import platform -import time from bluetooth_auto_recovery import recover_adapter from homeassistant.core import callback +from homeassistant.util.dt import monotonic_time_coarse from .const import ( DEFAULT_ADAPTER_BY_PLATFORM, @@ -29,7 +29,7 @@ async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBlea bluez_dbus = BlueZDBusObjects() await bluez_dbus.load() - now = time.monotonic() + now = monotonic_time_coarse() return { address: BluetoothServiceInfoBleak( name=history.advertisement_data.local_name diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 284e605fdfa5fd..7c8064d5583d04 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -19,6 +19,7 @@ ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse TWO_CHAR = re.compile("..") @@ -84,7 +85,7 @@ def discovered_devices_and_advertisement_data( @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" - now = time.monotonic() + now = monotonic_time_coarse() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper name = adv.name if prev_discovery := self._discovered_device_advertisement_datas.get(address): diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 80b322c1a14c8f..44e4403d689dd5 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,7 +4,9 @@ import bisect from contextlib import suppress import datetime as dt +import platform import re +import time from typing import Any import zoneinfo @@ -13,6 +15,7 @@ DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc +CLOCK_MONOTONIC_COARSE = 6 # EPOCHORDINAL is not exposed as a constant # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 @@ -461,3 +464,26 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: assert dattim.tzinfo is not None opposite_fold = dattim.replace(fold=not dattim.fold) return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() + + +def __monotonic_time_coarse() -> float: + """Return a monotonic time in seconds. + + This is the coarse version of time_monotonic, which is faster but less accurate. + + Since many arm64 and 32-bit platforms don't support VDSO with time.monotonic + because of errata, we can't rely on the kernel to provide a fast + monotonic time. + + https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ + """ + return 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 + ): + monotonic_time_coarse = __monotonic_time_coarse diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 79cd4e5e0dfdea..e902176bb35471 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta +import time import pytest @@ -719,3 +720,8 @@ def test_find_next_time_expression_tenth_second_pattern_does_not_drift_entering_ assert (next_target - prev_target).total_seconds() == 60 assert next_target.second == 10 prev_target = next_target + + +def test_monotonic_time_coarse(): + """Test monotonic time coarse.""" + assert abs(time.monotonic() - dt_util.monotonic_time_coarse()) < 1 From c36260dd17e2ac4e64362d796076e35b79260401 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 18:02:54 -0500 Subject: [PATCH 080/394] Move esphome gatt services cache to be per device (#81265) --- .../components/esphome/bluetooth/client.py | 6 +++--- .../components/esphome/domain_data.py | 20 ------------------- .../components/esphome/entry_data.py | 20 ++++++++++++++++++- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 9094186226f185..6be722976c56c7 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -255,9 +255,9 @@ async def get_services( A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. """ address_as_int = self._address_as_int - domain_data = self.domain_data + entry_data = self.entry_data if dangerous_use_bleak_cache and ( - cached_services := domain_data.get_gatt_services_cache(address_as_int) + cached_services := entry_data.get_gatt_services_cache(address_as_int) ): _LOGGER.debug( "Cached services hit for %s - %s", @@ -301,7 +301,7 @@ async def get_services( self._ble_device.name, self._ble_device.address, ) - domain_data.set_gatt_services_cache(address_as_int, services) + entry_data.set_gatt_services_cache(address_as_int, services) return services def _resolve_characteristic( diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index acaa76185e76a8..01f0a4d6b1369b 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -1,13 +1,9 @@ """Support for esphome domain data.""" from __future__ import annotations -from collections.abc import MutableMapping from dataclasses import dataclass, field from typing import TypeVar, cast -from bleak.backends.service import BleakGATTServiceCollection -from lru import LRU # pylint: disable=no-name-in-module - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder @@ -17,7 +13,6 @@ STORAGE_VERSION = 1 DOMAIN = "esphome" -MAX_CACHED_SERVICES = 128 _DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") @@ -29,21 +24,6 @@ class DomainData: _entry_datas: dict[str, RuntimeEntryData] = field(default_factory=dict) _stores: dict[str, Store] = field(default_factory=dict) _entry_by_unique_id: dict[str, ConfigEntry] = field(default_factory=dict) - _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( - default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] - ) - - def get_gatt_services_cache( - self, address: int - ) -> BleakGATTServiceCollection | None: - """Get the BleakGATTServiceCollection for the given address.""" - return self._gatt_services_cache.get(address) - - def set_gatt_services_cache( - self, address: int, services: BleakGATTServiceCollection - ) -> None: - """Set the BleakGATTServiceCollection for the given address.""" - self._gatt_services_cache[address] = services def get_by_unique_id(self, unique_id: str) -> ConfigEntry: """Get the config entry by its unique ID.""" diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index ac2a148d89913d..5d474b0fb15855 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, MutableMapping from dataclasses import dataclass, field import logging from typing import Any, cast @@ -30,6 +30,8 @@ UserService, ) from aioesphomeapi.model import ButtonInfo +from bleak.backends.service import BleakGATTServiceCollection +from lru import LRU # pylint: disable=no-name-in-module from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -57,6 +59,7 @@ SwitchInfo: Platform.SWITCH, TextSensorInfo: Platform.SENSOR, } +MAX_CACHED_SERVICES = 128 @dataclass @@ -92,6 +95,21 @@ class RuntimeEntryData: _ble_connection_free_futures: list[asyncio.Future[int]] = field( default_factory=list ) + _gatt_services_cache: MutableMapping[int, BleakGATTServiceCollection] = field( + default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] + ) + + def get_gatt_services_cache( + self, address: int + ) -> BleakGATTServiceCollection | None: + """Get the BleakGATTServiceCollection for the given address.""" + return self._gatt_services_cache.get(address) + + def set_gatt_services_cache( + self, address: int, services: BleakGATTServiceCollection + ) -> None: + """Set the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache[address] = services @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: From 5e3fb6ee9fe038a0ad29dd8e5d5d9119de363708 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 17:43:09 -0500 Subject: [PATCH 081/394] Provide a human readable error when an esphome ble proxy connection fails (#81266) --- homeassistant/components/esphome/bluetooth/client.py | 12 +++++++++++- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 6be722976c56c7..918d93f3d2cb6c 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,6 +7,7 @@ from typing import Any, TypeVar, cast import uuid +from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic @@ -182,8 +183,17 @@ def _on_bluetooth_connection_state( return if error: + try: + ble_connection_error = BLEConnectionError(error) + ble_connection_error_name = ble_connection_error.name + human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] + except (KeyError, ValueError): + ble_connection_error_name = str(error) + human_error = f"Unknown error code {error}" connected_future.set_exception( - BleakError(f"Error while connecting: {error}") + BleakError( + f"Error {ble_connection_error_name} while connecting: {human_error}" + ) ) return diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ab33ed8585a661..c0230ce841084c 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==11.2.0"], + "requirements": ["aioesphomeapi==11.3.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index b3b5a00e8b5474..8fb8163c6c712a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.2.0 +aioesphomeapi==11.3.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54ae8f373e3d49..b15626d1ba2112 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.2.0 +aioesphomeapi==11.3.0 # homeassistant.components.flo aioflo==2021.11.0 From 94f92e7f8aa702efaed7679959abcdeaa6ce3dde Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:27:04 -0500 Subject: [PATCH 082/394] Try to switch to a different esphome BLE proxy if we run out of slots while connecting (#81268) --- homeassistant/components/bluetooth/models.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a2e50fe1182256..a63a704baf6c7b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -264,6 +264,7 @@ def __init__( # pylint: disable=super-init-not-called, keyword-arg-before-varar self.__address = address_or_ble_device self.__disconnected_callback = disconnected_callback self.__timeout = timeout + self.__ble_device: BLEDevice | None = None self._backend: BaseBleakClient | None = None # type: ignore[assignment] @property @@ -283,14 +284,21 @@ def set_disconnected_callback( async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" - if not self._backend: + if ( + not self._backend + or not self.__ble_device + or not self._async_get_backend_for_ble_device(self.__ble_device) + ): assert MANAGER is not None wrapped_backend = ( self._async_get_backend() or self._async_get_fallback_backend() ) - self._backend = wrapped_backend.client( + self.__ble_device = ( await freshen_ble_device(wrapped_backend.device) - or wrapped_backend.device, + or wrapped_backend.device + ) + self._backend = wrapped_backend.client( + self.__ble_device, disconnected_callback=self.__disconnected_callback, timeout=self.__timeout, hass=MANAGER.hass, From 8bafb56f0422ce5d8ec8e56278526e8e4e973c50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 17:38:09 -0500 Subject: [PATCH 083/394] Bump bleak-retry-connector to 2.6.0 (#81270) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f8d1867035d376..261b4480671098 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.5.0", + "bleak-retry-connector==2.6.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d48b85e346b80c..6762357d58d029 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 8fb8163c6c712a..823073fc317c9a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b15626d1ba2112..51b4e5ad7ce79e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.5.0 +bleak-retry-connector==2.6.0 # homeassistant.components.bluetooth bleak==0.19.1 From e26149d0c34653ce21c879a38428f4d5940d8bf8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 19:24:14 -0500 Subject: [PATCH 084/394] Bump aioesphomeapi to 11.4.0 (#81277) --- 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 c0230ce841084c..cab81882788b8b 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==11.3.0"], + "requirements": ["aioesphomeapi==11.4.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 823073fc317c9a..893c4c05effb2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.3.0 +aioesphomeapi==11.4.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51b4e5ad7ce79e..41e2cc92f63f57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.3.0 +aioesphomeapi==11.4.0 # homeassistant.components.flo aioflo==2021.11.0 From 9fac632dcd064f6f895ef9a5cc3f34b3fbb5cfaa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 19:24:32 -0500 Subject: [PATCH 085/394] Bump bleak-retry-connector to 2.7.0 (#81280) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 261b4480671098..6b799e94e55650 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.6.0", + "bleak-retry-connector==2.7.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6762357d58d029..f75f7ba60da9ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 893c4c05effb2a..e3fdcfb37469e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 41e2cc92f63f57..baf6bff2e324f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.6.0 +bleak-retry-connector==2.7.0 # homeassistant.components.bluetooth bleak==0.19.1 From eccf61a546a4654c20544b371d2856ea1cdfa58b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 20:39:34 -0500 Subject: [PATCH 086/394] Bump aioesphomeapi to 11.4.1 (#81282) --- 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 cab81882788b8b..c27e3b8dc3e114 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==11.4.0"], + "requirements": ["aioesphomeapi==11.4.1"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index e3fdcfb37469e5..0e001a54c7e425 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.0 +aioesphomeapi==11.4.1 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baf6bff2e324f2..885b26e24bc635 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.0 +aioesphomeapi==11.4.1 # homeassistant.components.flo aioflo==2021.11.0 From 81dde5cfdf6c1fc5ef5ccc82b01abb05b9b94251 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 20:40:01 -0500 Subject: [PATCH 087/394] Bump bleak-retry-connector to 2.8.0 (#81283) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 6b799e94e55650..660345606c8788 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.7.0", + "bleak-retry-connector==2.8.0", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f75f7ba60da9ba..994a8d44019670 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 0e001a54c7e425..e35cdbeee7714a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 885b26e24bc635..23f1b6122d8d46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.7.0 +bleak-retry-connector==2.8.0 # homeassistant.components.bluetooth bleak==0.19.1 From 1f70941f6daea91af735749e07be6f3c9e519aee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 22:10:30 -0500 Subject: [PATCH 088/394] Do not fire the esphome ble disconnected callback if we were not connected (#81286) --- homeassistant/components/esphome/bluetooth/client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 918d93f3d2cb6c..68f1788afdbf87 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -127,13 +127,15 @@ def _unsubscribe_connection_state(self) -> None: def _async_ble_device_disconnected(self) -> None: """Handle the BLE device disconnecting from the ESP.""" - _LOGGER.debug("%s: BLE device disconnected", self._source) - self._is_connected = False + was_connected = self._is_connected self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] + self._is_connected = False if self._disconnected_event: self._disconnected_event.set() self._disconnected_event = None - self._async_call_bleak_disconnected_callback() + if was_connected: + _LOGGER.debug("%s: BLE device disconnected", self._source) + self._async_call_bleak_disconnected_callback() self._unsubscribe_connection_state() def _async_esp_disconnected(self) -> None: From 3cf63ec88ed0dd7d37318083293d59da3dc8dc51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 00:31:37 -0500 Subject: [PATCH 089/394] Include esphome device name in BLE logs (#81284) * Include esphome device name in BLE logs This makes it easier to debug what is going on when there are multiple esphome proxies * revert unintended change --- homeassistant/components/esphome/__init__.py | 2 + .../components/esphome/bluetooth/__init__.py | 8 +-- .../components/esphome/bluetooth/client.py | 53 +++++++++++++++---- .../components/esphome/entry_data.py | 17 ++++-- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a5428f7d6c5fec..23b6a6550e4889 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -249,6 +249,8 @@ async def on_connect() -> None: async def on_disconnect() -> None: """Run disconnect callbacks on API disconnect.""" + name = entry_data.device_info.name if entry_data.device_info else host + _LOGGER.debug("%s: %s disconnected, running disconnected callbacks", name, host) for disconnect_cb in entry_data.disconnect_callbacks: disconnect_cb() entry_data.disconnect_callbacks = [] diff --git a/homeassistant/components/esphome/bluetooth/__init__.py b/homeassistant/components/esphome/bluetooth/__init__.py index b4d5fdbd04df14..b5be5362474858 100644 --- a/homeassistant/components/esphome/bluetooth/__init__.py +++ b/homeassistant/components/esphome/bluetooth/__init__.py @@ -30,13 +30,15 @@ def _async_can_connect_factory( @hass_callback def _async_can_connect() -> bool: """Check if a given source can make another connection.""" + can_connect = bool(entry_data.available and entry_data.ble_connections_free) _LOGGER.debug( - "Checking if %s can connect, available=%s, ble_connections_free=%s", + "%s: Checking can connect, available=%s, ble_connections_free=%s result=%s", source, entry_data.available, entry_data.ble_connections_free, + can_connect, ) - return bool(entry_data.available and entry_data.ble_connections_free) + return can_connect return _async_can_connect @@ -55,7 +57,7 @@ async def async_connect_scanner( version = entry_data.device_info.bluetooth_proxy_version connectable = version >= 2 _LOGGER.debug( - "Connecting scanner for %s, version=%s, connectable=%s", + "%s: Connecting scanner version=%s, connectable=%s", source, version, connectable, diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 68f1788afdbf87..5f20a73f4d6316 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -61,7 +61,7 @@ async def _async_wrap_bluetooth_connected_operation( if disconnected_event.is_set(): task.cancel() raise BleakError( - f"{self._ble_device.name} ({self._ble_device.address}): " # pylint: disable=protected-access + f"{self._source}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access "Disconnected during operation" ) return next(iter(done)).result() @@ -120,7 +120,10 @@ def _unsubscribe_connection_state(self) -> None: self._cancel_connection_state() except (AssertionError, ValueError) as ex: _LOGGER.debug( - "Failed to unsubscribe from connection state (likely connection dropped): %s", + "%s: %s - %s: Failed to unsubscribe from connection state (likely connection dropped): %s", + self._source, + self._ble_device.name, + self._ble_device.address, ex, ) self._cancel_connection_state = None @@ -134,13 +137,23 @@ def _async_ble_device_disconnected(self) -> None: self._disconnected_event.set() self._disconnected_event = None if was_connected: - _LOGGER.debug("%s: BLE device disconnected", self._source) + _LOGGER.debug( + "%s: %s - %s: BLE device disconnected", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self._async_call_bleak_disconnected_callback() self._unsubscribe_connection_state() def _async_esp_disconnected(self) -> None: """Handle the esp32 client disconnecting from hass.""" - _LOGGER.debug("%s: ESP device disconnected", self._source) + _LOGGER.debug( + "%s: %s - %s: ESP device disconnected", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self.entry_data.disconnect_callbacks.remove(self._async_esp_disconnected) self._async_ble_device_disconnected() @@ -170,7 +183,10 @@ def _on_bluetooth_connection_state( ) -> None: """Handle a connect or disconnect.""" _LOGGER.debug( - "Connection state changed: connected=%s mtu=%s error=%s", + "%s: %s - %s: Connection state changed to connected=%s mtu=%s error=%s", + self._source, + self._ble_device.name, + self._ble_device.address, connected, mtu, error, @@ -203,6 +219,12 @@ def _on_bluetooth_connection_state( connected_future.set_exception(BleakError("Disconnected")) return + _LOGGER.debug( + "%s: %s - %s: connected, registering for disconnected callbacks", + self._source, + self._ble_device.name, + self._ble_device.address, + ) self.entry_data.disconnect_callbacks.append(self._async_esp_disconnected) connected_future.set_result(connected) @@ -230,7 +252,10 @@ async def _wait_for_free_connection_slot(self, timeout: float) -> None: if self.entry_data.ble_connections_free: return _LOGGER.debug( - "%s: Out of connection slots, waiting for a free one", self._source + "%s: %s - %s: Out of connection slots, waiting for a free one", + self._source, + self._ble_device.name, + self._ble_device.address, ) async with async_timeout.timeout(timeout): await self.entry_data.wait_for_ble_connections_free() @@ -272,20 +297,29 @@ async def get_services( cached_services := entry_data.get_gatt_services_cache(address_as_int) ): _LOGGER.debug( - "Cached services hit for %s - %s", + "%s: %s - %s: Cached services hit", + self._source, self._ble_device.name, self._ble_device.address, ) self.services = cached_services return self.services _LOGGER.debug( - "Cached services miss for %s - %s", + "%s: %s - %s: Cached services miss", + self._source, self._ble_device.name, self._ble_device.address, ) esphome_services = await self._client.bluetooth_gatt_get_services( address_as_int ) + _LOGGER.debug( + "%s: %s - %s: Got services: %s", + self._source, + self._ble_device.name, + self._ble_device.address, + esphome_services, + ) max_write_without_response = self.mtu_size - GATT_HEADER_SIZE services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] for service in esphome_services.services: @@ -309,7 +343,8 @@ async def get_services( ) self.services = services _LOGGER.debug( - "Cached services saved for %s - %s", + "%s: %s - %s: Cached services saved", + self._source, self._ble_device.name, self._ble_device.address, ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 5d474b0fb15855..faa9074b880e88 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -99,6 +99,11 @@ class RuntimeEntryData: default_factory=lambda: LRU(MAX_CACHED_SERVICES) # type: ignore[no-any-return] ) + @property + def name(self) -> str: + """Return the name of the device.""" + return self.device_info.name if self.device_info else self.entry_id + def get_gatt_services_cache( self, address: int ) -> BleakGATTServiceCollection | None: @@ -114,8 +119,13 @@ def set_gatt_services_cache( @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" - name = self.device_info.name if self.device_info else self.entry_id - _LOGGER.debug("%s: BLE connection limits: %s/%s", name, free, limit) + _LOGGER.debug( + "%s: BLE connection limits: used=%s free=%s limit=%s", + self.name, + limit - free, + free, + limit, + ) self.ble_connections_free = free self.ble_connections_limit = limit if free: @@ -186,7 +196,8 @@ def async_update_state(self, state: EntityState) -> None: subscription_key = (type(state), state.key) self.state[type(state)][state.key] = state _LOGGER.debug( - "Dispatching update with key %s: %s", + "%s: dispatching update with key %s: %s", + self.name, subscription_key, state, ) From 13562d271e664668c7372c8e082e8b2132a64222 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 00:28:38 -0500 Subject: [PATCH 090/394] Bump bleak-retry-connector to 2.8.1 (#81285) * Bump bleak-retry-connector to 2.8.1 reduces logging now that we have found the problem with esphome devices not disconnecting ble devices after timeout changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.0...v2.8.1 * empty --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 660345606c8788..091962fbc83294 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.0", + "bleak-retry-connector==2.8.1", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.59.1" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 994a8d44019670..914731a81649c6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index e35cdbeee7714a..332e98af7ed588 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23f1b6122d8d46..d7d7692aa3571b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.0 +bleak-retry-connector==2.8.1 # homeassistant.components.bluetooth bleak==0.19.1 From 8f843b3046ee9d381fd7cf904af50cf7f51aca81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Oct 2022 22:10:30 -0500 Subject: [PATCH 091/394] Do not fire the esphome ble disconnected callback if we were not connected (#81286) From 8eef55ed60d3ac945e51a282f0d02a22759f8a52 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 31 Oct 2022 03:23:05 -0500 Subject: [PATCH 092/394] Bump pyipp to 0.12.1 (#81287) bump pyipp to 0.12.1 --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index aadfdc8feeabb8..b673a2d5a6dccf 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -3,7 +3,7 @@ "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", "integration_type": "device", - "requirements": ["pyipp==0.12.0"], + "requirements": ["pyipp==0.12.1"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 332e98af7ed588..8e21a9ad125240 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1631,7 +1631,7 @@ pyintesishome==1.8.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.12.0 +pyipp==0.12.1 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7d7692aa3571b..4c909787a9f5a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1147,7 +1147,7 @@ pyinsteon==1.2.0 pyipma==3.0.5 # homeassistant.components.ipp -pyipp==0.12.0 +pyipp==0.12.1 # homeassistant.components.iqvia pyiqvia==2022.04.0 From 4fbbb7ba6dfd7bdecd662e47977bb7abe92f25ac Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 31 Oct 2022 11:09:15 +0100 Subject: [PATCH 093/394] Bump pyatmo to 7.3.0 (#81290) * Bump pyatmo to 7.3.0 * Update test fixture data and tests --- .../components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../netatmo/fixtures/getstationsdata.json | 222 ++++-- .../netatmo/fixtures/homesdata.json | 236 +++--- .../homestatus_91763b24c43d3e344f424e8b.json | 696 ++---------------- .../homestatus_91763b24c43d3e344f424e8c.json | 18 +- tests/components/netatmo/test_camera.py | 21 +- tests/components/netatmo/test_climate.py | 18 +- tests/components/netatmo/test_light.py | 12 +- tests/components/netatmo/test_sensor.py | 34 +- 11 files changed, 383 insertions(+), 880 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 5ad0fca3d7adca..e34156ff589d23 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.2.0"], + "requirements": ["pyatmo==7.3.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index 8e21a9ad125240..35856c010b869a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1442,7 +1442,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c909787a9f5a6..25febf43e637d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1030,7 +1030,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.2.0 +pyatmo==7.3.0 # homeassistant.components.apple_tv pyatv==0.10.3 diff --git a/tests/components/netatmo/fixtures/getstationsdata.json b/tests/components/netatmo/fixtures/getstationsdata.json index 822a4c11a5016a..10c3ca85e06c2c 100644 --- a/tests/components/netatmo/fixtures/getstationsdata.json +++ b/tests/components/netatmo/fixtures/getstationsdata.json @@ -114,7 +114,7 @@ "battery_percent": 79 }, { - "_id": "12:34:56:03:1b:e4", + "_id": "12:34:56:03:1b:e5", "type": "NAModule2", "module_name": "Garden", "data_type": ["Wind"], @@ -430,63 +430,203 @@ "modules": [] }, { - "_id": "12:34:56:58:c8:54", - "date_setup": 1605594014, - "last_setup": 1605594014, + "_id": "12:34:56:80:bb:26", + "station_name": "MYHOME (Palier)", + "date_setup": 1558709904, + "last_setup": 1558709904, "type": "NAMain", - "last_status_store": 1605878352, - "firmware": 178, - "wifi_status": 47, + "last_status_store": 1644582700, + "module_name": "Palier", + "firmware": 181, + "last_upgrade": 1558709906, + "wifi_status": 57, "reachable": true, "co2_calibrating": false, "data_type": ["Temperature", "CO2", "Humidity", "Noise", "Pressure"], "place": { - "altitude": 65, - "city": "Njurunda District", - "country": "SE", - "timezone": "Europe/Stockholm", - "location": [17.123456, 62.123456] + "altitude": 329, + "city": "Someplace", + "country": "FR", + "timezone": "Europe/Paris", + "location": [6.1234567, 46.123456] }, - "station_name": "Njurunda (Indoor)", - "home_id": "5fb36b9ec68fd10c6467ca65", - "home_name": "Njurunda", + "home_id": "91763b24c43d3e344f424e8b", + "home_name": "MYHOME", "dashboard_data": { - "time_utc": 1605878349, - "Temperature": 19.7, - "CO2": 993, - "Humidity": 40, - "Noise": 40, - "Pressure": 1015.6, - "AbsolutePressure": 1007.8, - "min_temp": 19.7, - "max_temp": 20.4, - "date_max_temp": 1605826917, - "date_min_temp": 1605873207, + "time_utc": 1644582694, + "Temperature": 21.1, + "CO2": 1339, + "Humidity": 45, + "Noise": 35, + "Pressure": 1026.8, + "AbsolutePressure": 974.5, + "min_temp": 21, + "max_temp": 21.8, + "date_max_temp": 1644534255, + "date_min_temp": 1644550420, "temp_trend": "stable", "pressure_trend": "up" }, "modules": [ { - "_id": "12:34:56:58:e6:38", + "_id": "12:34:56:80:1c:42", "type": "NAModule1", - "last_setup": 1605594034, + "module_name": "Outdoor", + "last_setup": 1558709954, "data_type": ["Temperature", "Humidity"], - "battery_percent": 100, + "battery_percent": 27, "reachable": true, "firmware": 50, - "last_message": 1605878347, - "last_seen": 1605878328, - "rf_status": 62, - "battery_vp": 6198, + "last_message": 1644582699, + "last_seen": 1644582699, + "rf_status": 68, + "battery_vp": 4678, "dashboard_data": { - "time_utc": 1605878328, - "Temperature": 0.6, - "Humidity": 77, - "min_temp": -2.1, - "max_temp": 1.5, - "date_max_temp": 1605865920, - "date_min_temp": 1605826904, - "temp_trend": "down" + "time_utc": 1644582648, + "Temperature": 9.4, + "Humidity": 57, + "min_temp": 6.7, + "max_temp": 9.8, + "date_max_temp": 1644534223, + "date_min_temp": 1644569369, + "temp_trend": "up" + } + }, + { + "_id": "12:34:56:80:c1:ea", + "type": "NAModule3", + "module_name": "Rain", + "last_setup": 1563734531, + "data_type": ["Rain"], + "battery_percent": 21, + "reachable": true, + "firmware": 12, + "last_message": 1644582699, + "last_seen": 1644582699, + "rf_status": 79, + "battery_vp": 4256, + "dashboard_data": { + "time_utc": 1644582686, + "Rain": 3.7, + "sum_rain_1": 0, + "sum_rain_24": 6.9 + } + }, + { + "_id": "12:34:56:80:44:92", + "type": "NAModule4", + "module_name": "Bedroom", + "last_setup": 1575915890, + "data_type": ["Temperature", "CO2", "Humidity"], + "battery_percent": 28, + "reachable": true, + "firmware": 51, + "last_message": 1644582699, + "last_seen": 1644582654, + "rf_status": 67, + "battery_vp": 4695, + "dashboard_data": { + "time_utc": 1644582654, + "Temperature": 19.3, + "CO2": 1076, + "Humidity": 53, + "min_temp": 19.2, + "max_temp": 19.7, + "date_max_temp": 1644534243, + "date_min_temp": 1644553418, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:80:7e:18", + "type": "NAModule4", + "module_name": "Bathroom", + "last_setup": 1575915955, + "data_type": ["Temperature", "CO2", "Humidity"], + "battery_percent": 55, + "reachable": true, + "firmware": 51, + "last_message": 1644582699, + "last_seen": 1644582654, + "rf_status": 59, + "battery_vp": 5184, + "dashboard_data": { + "time_utc": 1644582654, + "Temperature": 19.4, + "CO2": 1930, + "Humidity": 55, + "min_temp": 19.4, + "max_temp": 21.8, + "date_max_temp": 1644534224, + "date_min_temp": 1644582039, + "temp_trend": "stable" + } + }, + { + "_id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "module_name": "Garden", + "data_type": ["Wind"], + "last_setup": 1549193862, + "reachable": true, + "dashboard_data": { + "time_utc": 1559413170, + "WindStrength": 4, + "WindAngle": 217, + "GustStrength": 9, + "GustAngle": 206, + "max_wind_str": 21, + "max_wind_angle": 217, + "date_max_wind_str": 1559386669 + }, + "firmware": 19, + "last_message": 1559413177, + "last_seen": 1559413177, + "rf_status": 59, + "battery_vp": 5689, + "battery_percent": 85 + } + ] + }, + { + "_id": "00:11:22:2c:be:c8", + "station_name": "Zuhause (Kinderzimmer)", + "type": "NAMain", + "last_status_store": 1649146022, + "reachable": true, + "favorite": true, + "data_type": ["Pressure"], + "place": { + "altitude": 127, + "city": "Wiesbaden", + "country": "DE", + "timezone": "Europe/Berlin", + "location": [8.238054275512695, 50.07585525512695] + }, + "read_only": true, + "dashboard_data": { + "time_utc": 1649146022, + "Pressure": 1015.6, + "AbsolutePressure": 1000.4, + "pressure_trend": "stable" + }, + "modules": [ + { + "_id": "00:11:22:2c:ce:b6", + "type": "NAModule1", + "data_type": ["Temperature", "Humidity"], + "reachable": true, + "last_message": 1649146022, + "last_seen": 1649145996, + "dashboard_data": { + "time_utc": 1649145996, + "Temperature": 7.8, + "Humidity": 87, + "min_temp": 6.5, + "max_temp": 7.8, + "date_max_temp": 1649145996, + "date_min_temp": 1649118465, + "temp_trend": "up" } } ] diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index 93c04388f4c190..6b24a7f8f9d4ac 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -23,7 +23,6 @@ "12:34:56:00:f1:62", "12:34:56:10:f1:66", "12:34:56:00:e3:9b", - "12:34:56:00:86:99", "0009999992" ] }, @@ -39,12 +38,6 @@ "type": "kitchen", "module_ids": ["12:34:56:03:a0:ac"] }, - { - "id": "2940411588", - "name": "Child", - "type": "custom", - "module_ids": ["12:34:56:26:cc:01"] - }, { "id": "222452125", "name": "Bureau", @@ -76,6 +69,12 @@ "name": "Corridor", "type": "corridor", "module_ids": ["10:20:30:bd:b8:1e"] + }, + { + "id": "100007520", + "name": "Toilettes", + "type": "toilets", + "module_ids": ["00:11:22:33:00:11:45:fe"] } ], "modules": [ @@ -120,15 +119,29 @@ "name": "Hall", "setup_date": 1544828430, "room_id": "3688132631", - "reachable": true, "modules_bridged": ["12:34:56:00:86:99", "12:34:56:00:e3:9b"] }, { - "id": "12:34:56:00:a5:a4", + "id": "12:34:56:10:f1:66", + "type": "NDB", + "name": "Netatmo-Doorbell", + "setup_date": 1602691361, + "room_id": "3688132631", + "reachable": true, + "hk_device_id": "123456007df1", + "customer_id": "1000010", + "network_lock": false, + "quick_display_zone": 62 + }, + { + "id": "12:34:56:10:b9:0e", "type": "NOC", - "name": "Garden", - "setup_date": 1544828430, - "reachable": true + "name": "Front", + "setup_date": 1509290599, + "reachable": true, + "customer_id": "A00010", + "network_lock": false, + "use_pincode": false }, { "id": "12:34:56:20:f5:44", @@ -155,33 +168,6 @@ "room_id": "222452125", "bridge": "12:34:56:20:f5:44" }, - { - "id": "12:34:56:10:f1:66", - "type": "NDB", - "name": "Netatmo-Doorbell", - "setup_date": 1602691361, - "room_id": "3688132631", - "reachable": true, - "hk_device_id": "123456007df1", - "customer_id": "1000010", - "network_lock": false, - "quick_display_zone": 62 - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "setup_date": 1620479901, - "bridge": "12:34:56:00:f1:62", - "name": "Sirene in hall" - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "name": "Window Hall", - "setup_date": 1581177375, - "bridge": "12:34:56:00:f1:62", - "category": "window" - }, { "id": "12:34:56:30:d5:d4", "type": "NBG", @@ -199,16 +185,17 @@ "bridge": "12:34:56:30:d5:d4" }, { - "id": "12:34:56:37:11:ca", + "id": "12:34:56:80:bb:26", "type": "NAMain", - "name": "NetatmoIndoor", + "name": "Villa", "setup_date": 1419453350, + "room_id": "4122897288", "reachable": true, "modules_bridged": [ - "12:34:56:07:bb:3e", - "12:34:56:03:1b:e4", - "12:34:56:36:fc:de", - "12:34:56:05:51:20" + "12:34:56:80:44:92", + "12:34:56:80:7e:18", + "12:34:56:80:1c:42", + "12:34:56:80:c1:ea" ], "customer_id": "C00016", "hardware_version": 251, @@ -271,48 +258,46 @@ "module_offset": { "12:34:56:80:bb:26": { "a": 0.1 + }, + "03:00:00:03:1b:0e": { + "a": 0 } } }, { - "id": "12:34:56:36:fc:de", + "id": "12:34:56:80:1c:42", "type": "NAModule1", "name": "Outdoor", "setup_date": 1448565785, - "bridge": "12:34:56:37:11:ca" - }, - { - "id": "12:34:56:03:1b:e4", - "type": "NAModule2", - "name": "Garden", - "setup_date": 1543579864, - "bridge": "12:34:56:37:11:ca" + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:05:51:20", + "id": "12:34:56:80:c1:ea", "type": "NAModule3", "name": "Rain", "setup_date": 1591770206, - "bridge": "12:34:56:37:11:ca" + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:07:bb:3e", + "id": "12:34:56:80:44:92", "type": "NAModule4", "name": "Bedroom", "setup_date": 1484997703, - "bridge": "12:34:56:37:11:ca" + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:26:68:92", - "type": "NHC", - "name": "Indoor", - "setup_date": 1571342643 + "id": "12:34:56:80:7e:18", + "type": "NAModule4", + "name": "Bathroom", + "setup_date": 1543579864, + "bridge": "12:34:56:80:bb:26" }, { - "id": "12:34:56:26:cc:01", - "type": "BNS", - "name": "Child", - "setup_date": 1571634243 + "id": "12:34:56:03:1b:e4", + "type": "NAModule2", + "name": "Garden", + "setup_date": 1543579864, + "bridge": "12:34:56:80:bb:26" }, { "id": "12:34:56:80:60:40", @@ -324,7 +309,8 @@ "12:34:56:80:00:12:ac:f2", "12:34:56:80:00:c3:69:3c", "12:34:56:00:00:a1:4c:da", - "12:34:56:00:01:01:01:a1" + "12:34:56:00:01:01:01:a1", + "00:11:22:33:00:11:45:fe" ] }, { @@ -342,6 +328,21 @@ "setup_date": 1641841262, "bridge": "12:34:56:80:60:40" }, + { + "id": "12:34:56:00:86:99", + "type": "NACamDoorTag", + "name": "Window Hall", + "setup_date": 1581177375, + "bridge": "12:34:56:00:f1:62", + "category": "window" + }, + { + "id": "12:34:56:00:e3:9b", + "type": "NIS", + "setup_date": 1620479901, + "bridge": "12:34:56:00:f1:62", + "name": "Sirene in hall" + }, { "id": "12:34:56:00:16:0e", "type": "NLE", @@ -440,6 +441,24 @@ "room_id": "100008999", "bridge": "12:34:56:80:60:40" }, + { + "id": "10:20:30:bd:b8:1e", + "type": "BNS", + "name": "Smarther", + "setup_date": 1638022197, + "room_id": "1002003001" + }, + { + "id": "00:11:22:33:00:11:45:fe", + "type": "NLF", + "on": false, + "brightness": 63, + "firmware_revision": 57, + "last_seen": 1657086939, + "power": 0, + "reachable": true, + "bridge": "12:34:56:80:60:40" + }, { "id": "12:34:56:00:01:01:01:a1", "type": "NLFN", @@ -761,80 +780,13 @@ "therm_mode": "schedule" }, { - "id": "111111111111111111111401", - "name": "Home with no modules", - "altitude": 9, - "coordinates": [1.23456789, 50.0987654], - "country": "BE", - "timezone": "Europe/Brussels", - "rooms": [ - { - "id": "1111111401", - "name": "Livingroom", - "type": "livingroom" - } - ], - "temperature_control_mode": "heating", - "therm_mode": "away", - "therm_setpoint_default_duration": 120, - "cooling_mode": "schedule", - "schedules": [ - { - "away_temp": 14, - "hg_temp": 7, - "name": "Week", - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 6, - "m_offset": 420 - } - ], - "zones": [ - { - "type": 0, - "name": "Comfort", - "rooms_temp": [], - "id": 0, - "rooms": [] - }, - { - "type": 1, - "name": "Nacht", - "rooms_temp": [], - "id": 1, - "rooms": [] - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [], - "id": 4, - "rooms": [] - }, - { - "type": 4, - "name": "Tussenin", - "rooms_temp": [], - "id": 5, - "rooms": [] - }, - { - "type": 4, - "name": "Ochtend", - "rooms_temp": [], - "id": 6, - "rooms": [] - } - ], - "id": "700000000000000000000401", - "selected": true, - "type": "therm" - } - ] + "id": "91763b24c43d3e344f424e8c", + "altitude": 112, + "coordinates": [52.516263, 13.377726], + "country": "DE", + "timezone": "Europe/Berlin", + "therm_setpoint_default_duration": 180, + "therm_mode": "schedule" } ], "user": { @@ -845,6 +797,8 @@ "unit_pressure": 0, "unit_system": 0, "unit_wind": 0, + "all_linked": false, + "type": "netatmo", "id": "91763b24c43d3e344f424e8b" } }, diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json index 4cd5dceec3bbb7..736d70be11cde3 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json @@ -14,25 +14,6 @@ "vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,", "is_local": true }, - { - "type": "NOC", - "firmware_revision": 3002000, - "monitoring": "on", - "sd_status": 4, - "connection": "wifi", - "homekit_status": "upgradable", - "floodlight": "auto", - "timelapse_available": true, - "id": "12:34:56:00:a5:a4", - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,", - "is_local": false, - "network_lock": false, - "firmware_name": "3.2.0", - "wifi_strength": 62, - "alim_status": 2, - "locked": false, - "wifi_state": "high" - }, { "id": "12:34:56:00:fa:d0", "type": "NAPlug", @@ -46,6 +27,7 @@ "type": "NATherm1", "firmware_revision": 65, "rf_strength": 58, + "battery_level": 3793, "boiler_valve_comfort_boost": false, "boiler_status": false, "anticipating": false, @@ -58,6 +40,7 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 51, + "battery_level": 3025, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, @@ -67,18 +50,10 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 59, + "battery_level": 3029, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, - { - "id": "12:34:56:26:cc:01", - "type": "BNS", - "firmware_revision": 32, - "wifi_strength": 50, - "boiler_valve_comfort_boost": false, - "boiler_status": true, - "cooler_status": false - }, { "type": "NDB", "last_ftp_event": { @@ -100,6 +75,25 @@ "wifi_strength": 66, "wifi_state": "medium" }, + { + "type": "NOC", + "firmware_revision": 3002000, + "monitoring": "on", + "sd_status": 4, + "connection": "wifi", + "homekit_status": "upgradable", + "floodlight": "auto", + "timelapse_available": true, + "id": "12:34:56:10:b9:0e", + "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,", + "is_local": false, + "network_lock": false, + "firmware_name": "3.2.0", + "wifi_strength": 62, + "alim_status": 2, + "locked": false, + "wifi_state": "high" + }, { "boiler_control": "onoff", "dhw_control": "none", @@ -264,629 +258,43 @@ "bridge": "12:34:56:80:60:40" }, { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, - "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, + "id": "10:20:30:bd:b8:1e", + "type": "BNS", + "firmware_revision": 32, + "wifi_strength": 49, "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true + "boiler_status": true, + "cooler_status": false }, { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, + "id": "00:11:22:33:00:11:45:fe", + "type": "NLF", + "on": false, + "brightness": 63, + "firmware_revision": 57, + "last_seen": 1657086939, "power": 0, "reachable": true, "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, + } + ], + "rooms": [ { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, + "id": "2746182631", "reachable": true, - "bridge": "12:34:56:80:60:40" + "therm_measured_temperature": 19.8, + "therm_setpoint_temperature": 12, + "therm_setpoint_mode": "away", + "therm_setpoint_start_time": 1559229567, + "therm_setpoint_end_time": 0 }, { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, + "id": "2940411577", "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, - "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true - }, - { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, - "power": 0, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:01:01:01:a1", - "brightness": 100, - "firmware_revision": 52, - "last_seen": 1604940167, - "on": false, - "power": 0, - "reachable": true, - "type": "NLFN", - "bridge": "12:34:56:80:60:40" - }, - { - "type": "NDB", - "last_ftp_event": { - "type": 3, - "time": 1631444443, - "id": 3 - }, - "id": "12:34:56:10:f1:66", - "websocket_connected": true, - "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,", - "is_local": false, - "alim_status": 2, - "connection": "wifi", - "firmware_name": "2.18.0", - "firmware_revision": 2018000, - "homekit_status": "configured", - "max_peers_reached": false, - "sd_status": 4, - "wifi_strength": 66, - "wifi_state": "medium" - }, - { - "boiler_control": "onoff", - "dhw_control": "none", - "firmware_revision": 22, - "hardware_version": 222, - "id": "12:34:56:20:f5:44", - "outdoor_temperature": 8.2, - "sequence_id": 19764, - "type": "OTH", - "wifi_strength": 57 - }, - { - "battery_level": 4176, - "boiler_status": false, - "boiler_valve_comfort_boost": false, - "firmware_revision": 6, - "id": "12:34:56:20:f5:8c", - "last_message": 1637684297, - "last_seen": 1637684297, - "radio_id": 2, - "reachable": true, - "rf_strength": 64, - "type": "OTM", - "bridge": "12:34:56:20:f5:44", - "battery_state": "full" - }, - { - "id": "12:34:56:30:d5:d4", - "type": "NBG", - "firmware_revision": 39, - "wifi_strength": 65, - "reachable": true - }, - { - "id": "0009999992", - "type": "NBR", - "current_position": 0, - "target_position": 0, - "target_position_step": 100, - "firmware_revision": 16, - "rf_strength": 0, - "last_seen": 1638353156, - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, - "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", - "therm_setpoint_start_time": 0, - "therm_setpoint_end_time": 0, - "anticipating": false, - "open_window": false - }, - { - "id": "12:34:56:00:86:99", - "type": "NACamDoorTag", - "battery_state": "high", - "battery_level": 5240, - "firmware_revision": 58, - "rf_state": "full", - "rf_strength": 58, - "last_seen": 1642698124, - "last_activity": 1627757310, - "reachable": false, - "bridge": "12:34:56:00:f1:62", - "status": "no_news" - }, - { - "id": "12:34:56:00:e3:9b", - "type": "NIS", - "battery_state": "low", - "battery_level": 5438, - "firmware_revision": 209, - "rf_state": "medium", - "rf_strength": 62, - "last_seen": 1644569790, - "reachable": true, - "bridge": "12:34:56:00:f1:62", - "status": "no_sound", - "monitoring": "off" - }, - { - "id": "12:34:56:80:60:40", - "type": "NLG", - "offload": false, - "firmware_revision": 211, - "last_seen": 1644567372, - "wifi_strength": 51, - "reachable": true - }, - { - "id": "12:34:56:80:00:12:ac:f2", - "type": "NLP", - "on": true, - "offload": false, - "firmware_revision": 62, - "last_seen": 1644569425, - "power": 0, - "reachable": true, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:80:00:c3:69:3c", - "type": "NLT", - "battery_state": "full", - "battery_level": 3300, - "firmware_revision": 42, - "last_seen": 0, - "reachable": false, - "bridge": "12:34:56:80:60:40" - }, - { - "id": "12:34:56:00:16:0e", - "type": "NLE", - "firmware_revision": 14, - "wifi_strength": 38 - }, - { - "id": "12:34:56:00:16:0e#0", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#1", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#2", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#3", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#4", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#5", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#6", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#7", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:16:0e#8", - "type": "NLE", - "bridge": "12:34:56:00:16:0e" - }, - { - "id": "12:34:56:00:00:a1:4c:da", - "type": "NLPC", - "firmware_revision": 62, - "last_seen": 1646511241, - "power": 476, - "reachable": true, - "bridge": "12:34:56:80:60:40" - } - ], - "rooms": [ - { - "id": "2746182631", - "reachable": true, - "therm_measured_temperature": 19.8, - "therm_setpoint_temperature": 12, - "therm_setpoint_mode": "schedule", - "therm_setpoint_start_time": 1559229567, - "therm_setpoint_end_time": 0 - }, - { - "id": "2940411577", - "reachable": true, - "therm_measured_temperature": 5, - "heating_power_request": 1, + "therm_measured_temperature": 27, + "heating_power_request": 0, "therm_setpoint_temperature": 7, - "therm_setpoint_mode": "away", + "therm_setpoint_mode": "hg", "therm_setpoint_start_time": 0, "therm_setpoint_end_time": 0, "anticipating": false, @@ -905,15 +313,15 @@ "open_window": false }, { - "id": "2940411588", + "id": "1002003001", "reachable": true, "anticipating": false, "heating_power_request": 0, "open_window": false, - "humidity": 68, - "therm_measured_temperature": 19.9, - "therm_setpoint_temperature": 21.5, - "therm_setpoint_start_time": 1647793285, + "humidity": 67, + "therm_measured_temperature": 22, + "therm_setpoint_temperature": 22, + "therm_setpoint_start_time": 1647462737, "therm_setpoint_end_time": null, "therm_setpoint_mode": "home" } diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json index d950c82a6a5ced..406e24bc1077dc 100644 --- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json @@ -1,12 +1,20 @@ { "status": "ok", - "time_server": 1559292041, + "time_server": 1642952130, "body": { "home": { - "modules": [], - "rooms": [], - "id": "91763b24c43d3e344f424e8c", - "persons": [] + "persons": [ + { + "id": "abcdef12-1111-0000-0000-000111222333", + "last_seen": 1489050910, + "out_of_sight": true + }, + { + "id": "abcdef12-2222-0000-0000-000111222333", + "last_seen": 1489078776, + "out_of_sight": true + } + ] } } } diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index beb91c7565e2d3..76397988187b49 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -33,7 +33,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): await hass.async_block_till_done() camera_entity_indoor = "camera.hall" - camera_entity_outdoor = "camera.garden" + camera_entity_outdoor = "camera.front" assert hass.states.get(camera_entity_indoor).state == "streaming" response = { "event_type": "off", @@ -59,8 +59,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "on", @@ -72,8 +72,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "auto", @@ -84,7 +84,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", } @@ -166,7 +166,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) uri = "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,," stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.garden" + camera_entity_indoor = "camera.front" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -304,14 +304,14 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.garden", + "entity_id": "camera.front", "camera_light_mode": "on", } expected_data = { "modules": [ { - "id": "12:34:56:00:a5:a4", + "id": "12:34:56:10:b9:0e", "floodlight": "on", }, ], @@ -353,7 +353,6 @@ async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo assert excinfo.value.args == ("NACamera does not have a floodlight",) -@pytest.mark.skip async def test_camera_reconnect_webhook(hass, config_entry): """Test webhook event on camera reconnect.""" fake_post_hits = 0 @@ -406,7 +405,7 @@ async def fake_post(*args, **kwargs): dt.utcnow() + timedelta(seconds=60), ) await hass.async_block_till_done() - assert fake_post_hits > calls + assert fake_post_hits >= calls async def test_webhook_person_event(hass, config_entry, netatmo_auth): diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index d37bab929e1518..afe85049f95d8d 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -36,8 +36,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 12 @@ -80,8 +79,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "heat" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 21 @@ -194,8 +192,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) @@ -213,8 +210,7 @@ async def test_service_preset_mode_frost_guard_thermostat( assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) # Test service setting the preset mode to "frost guard" @@ -269,8 +265,7 @@ async def test_service_preset_mode_frost_guard_thermostat( assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) @@ -286,8 +281,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth) assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( - hass.states.get(climate_entity_livingroom).attributes["preset_mode"] - == "Schedule" + hass.states.get(climate_entity_livingroom).attributes["preset_mode"] == "away" ) # Test service setting the preset mode to "away" diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index b1a5270745ce87..526fb2fe518b48 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -27,14 +27,14 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await hass.async_block_till_done() - light_entity = "light.garden" + light_entity = "light.front" assert hass.states.get(light_entity).state == "unavailable" # Trigger light mode change response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", - "camera_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", + "camera_id": "12:34:56:10:b9:0e", "event_id": "601dce1560abca1ebad9b723", "push_type": "NOC-light_mode", "sub_type": "on", @@ -46,7 +46,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) # Trigger light mode change with erroneous webhook data response = { "event_type": "light_mode", - "device_id": "12:34:56:00:a5:a4", + "device_id": "12:34:56:10:b9:0e", } await simulate_webhook(hass, webhook_id, response) @@ -62,7 +62,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "auto"}]} + {"modules": [{"id": "12:34:56:10:b9:0e", "floodlight": "auto"}]} ) # Test turning light on @@ -75,7 +75,7 @@ async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth) ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( - {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "on"}]} + {"modules": [{"id": "12:34:56:10:b9:0e", "floodlight": "on"}]} ) diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index d3ea8fb8167a65..9ef5637231615c 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -16,12 +16,12 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): await hass.async_block_till_done() - prefix = "sensor.netatmoindoor_" + prefix = "sensor.parents_bedroom_" - assert hass.states.get(f"{prefix}temperature").state == "24.6" - assert hass.states.get(f"{prefix}humidity").state == "36" - assert hass.states.get(f"{prefix}co2").state == "749" - assert hass.states.get(f"{prefix}pressure").state == "1017.3" + assert hass.states.get(f"{prefix}temperature").state == "20.3" + assert hass.states.get(f"{prefix}humidity").state == "63" + assert hass.states.get(f"{prefix}co2").state == "494" + assert hass.states.get(f"{prefix}pressure").state == "1014.5" async def test_public_weather_sensor(hass, config_entry, netatmo_auth): @@ -104,25 +104,25 @@ async def test_process_health(health, expected): @pytest.mark.parametrize( "uid, name, expected", [ - ("12:34:56:37:11:ca-reachable", "mystation_reachable", "True"), - ("12:34:56:03:1b:e4-rf_status", "mystation_yard_radio", "Full"), + ("12:34:56:03:1b:e4-reachable", "villa_garden_reachable", "True"), + ("12:34:56:03:1b:e4-rf_status", "villa_garden_radio", "Full"), ( - "12:34:56:37:11:ca-wifi_status", - "mystation_wifi_strength", - "Full", + "12:34:56:80:bb:26-wifi_status", + "villa_wifi_strength", + "High", ), ( - "12:34:56:37:11:ca-temp_trend", - "mystation_temperature_trend", + "12:34:56:80:bb:26-temp_trend", + "villa_temperature_trend", "stable", ), ( - "12:34:56:37:11:ca-pressure_trend", - "netatmo_mystation_pressure_trend", - "down", + "12:34:56:80:bb:26-pressure_trend", + "villa_pressure_trend", + "up", ), - ("12:34:56:05:51:20-sum_rain_1", "netatmo_mystation_yard_rain_last_hour", "0"), - ("12:34:56:05:51:20-sum_rain_24", "netatmo_mystation_yard_rain_today", "0"), + ("12:34:56:80:c1:ea-sum_rain_1", "villa_rain_rain_last_hour", "0"), + ("12:34:56:80:c1:ea-sum_rain_24", "villa_rain_rain_today", "6.9"), ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"), ( "12:34:56:03:1b:e4-windangle_value", From f3a96ce14b3d1cba01c06b267b47cda93c3cfc7e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 08:18:49 -0500 Subject: [PATCH 094/394] Bump dbus-fast to 1.60.0 (#81296) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 091962fbc83294..bca2f7f9a8d1c7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.1", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.59.1" + "dbus-fast==1.60.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 914731a81649c6..e2c57d87c19c03 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.59.1 +dbus-fast==1.60.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 35856c010b869a..bfa8a46021cae3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.1 +dbus-fast==1.60.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25febf43e637d6..6fbc1a59d745c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.59.1 +dbus-fast==1.60.0 # homeassistant.components.debugpy debugpy==1.6.3 From 0a476baf16014f26cd67ad9fbbdf166bb3d3d7b8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Oct 2022 09:54:14 -0400 Subject: [PATCH 095/394] Bumped version to 2022.11.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3f25ea89c09e0c..5acf294fb68112 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 5a9507f8dfa226..16ff4bc6bbef85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b3" +version = "2022.11.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 82151bfd40f7b64044a5a9f82f020411430df97b Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 31 Oct 2022 09:57:54 -0400 Subject: [PATCH 096/394] Create repairs for unsupported and unhealthy (#80747) --- homeassistant/components/hassio/__init__.py | 6 + homeassistant/components/hassio/const.py | 21 +- homeassistant/components/hassio/handler.py | 8 + homeassistant/components/hassio/repairs.py | 138 ++++++ homeassistant/components/hassio/strings.json | 10 + tests/components/hassio/test_binary_sensor.py | 13 + tests/components/hassio/test_diagnostics.py | 13 + tests/components/hassio/test_init.py | 43 +- tests/components/hassio/test_repairs.py | 395 ++++++++++++++++++ tests/components/hassio/test_sensor.py | 13 + tests/components/hassio/test_update.py | 13 + tests/components/hassio/test_websocket_api.py | 13 + tests/components/http/test_ban.py | 12 +- tests/components/onboarding/test_views.py | 13 + 14 files changed, 690 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/hassio/repairs.py create mode 100644 tests/components/hassio/test_repairs.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8535a0c3cc6eb1..c811b35812e7ea 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -77,6 +77,7 @@ from .handler import HassIO, HassioAPIError, api_data from .http import HassIOView from .ingress import async_setup_ingress_view +from .repairs import SupervisorRepairs from .websocket_api import async_load_websocket_api _LOGGER = logging.getLogger(__name__) @@ -103,6 +104,7 @@ DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" +DATA_SUPERVISOR_REPAIRS = "supervisor_repairs" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) ADDONS_COORDINATOR = "hassio_addons_coordinator" @@ -758,6 +760,10 @@ async def _async_setup_hardware_integration(hass): hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) ) + # Start listening for problems with supervisor and making repairs + hass.data[DATA_SUPERVISOR_REPAIRS] = repairs = SupervisorRepairs(hass, hassio) + await repairs.setup() + return True diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index e37a31ddbd6cf5..64ef7a718a5cbe 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -11,19 +11,26 @@ ATTR_DATA = "data" ATTR_DISCOVERY = "discovery" ATTR_ENABLE = "enable" +ATTR_ENDPOINT = "endpoint" ATTR_FOLDERS = "folders" +ATTR_HEALTHY = "healthy" ATTR_HOMEASSISTANT = "homeassistant" ATTR_INPUT = "input" +ATTR_METHOD = "method" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" +ATTR_RESULT = "result" +ATTR_SUPPORTED = "supported" +ATTR_TIMEOUT = "timeout" ATTR_TITLE = "title" +ATTR_UNHEALTHY = "unhealthy" +ATTR_UNHEALTHY_REASONS = "unhealthy_reasons" +ATTR_UNSUPPORTED = "unsupported" +ATTR_UNSUPPORTED_REASONS = "unsupported_reasons" +ATTR_UPDATE_KEY = "update_key" ATTR_USERNAME = "username" ATTR_UUID = "uuid" ATTR_WS_EVENT = "event" -ATTR_ENDPOINT = "endpoint" -ATTR_METHOD = "method" -ATTR_RESULT = "result" -ATTR_TIMEOUT = "timeout" X_AUTH_TOKEN = "X-Supervisor-Token" X_INGRESS_PATH = "X-Ingress-Path" @@ -38,6 +45,11 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" +EVENT_SUPERVISOR_UPDATE = "supervisor_update" +EVENT_HEALTH_CHANGED = "health_changed" +EVENT_SUPPORTED_CHANGED = "supported_changed" + +UPDATE_KEY_SUPERVISOR = "supervisor" ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" @@ -51,7 +63,6 @@ ATTR_URL = "url" ATTR_REPOSITORY = "repository" - DATA_KEY_ADDONS = "addons" DATA_KEY_OS = "os" DATA_KEY_SUPERVISOR = "supervisor" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7b3ed697227e77..ee16bdf815869b 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -190,6 +190,14 @@ def get_discovery_message(self, uuid): """ return self.send_command(f"/discovery/{uuid}", method="get") + @api_data + def get_resolution_info(self): + """Return data for Supervisor resolution center. + + This method return a coroutine. + """ + return self.send_command("/resolution/info", method="get") + @_api_bool async def update_hass_api(self, http_config, refresh_token): """Update Home Assistant API data on Hass.io.""" diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py new file mode 100644 index 00000000000000..a8c6788f4d5983 --- /dev/null +++ b/homeassistant/components/hassio/repairs.py @@ -0,0 +1,138 @@ +"""Supervisor events monitor.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) + +from .const import ( + ATTR_DATA, + ATTR_HEALTHY, + ATTR_SUPPORTED, + ATTR_UNHEALTHY, + ATTR_UNHEALTHY_REASONS, + ATTR_UNSUPPORTED, + ATTR_UNSUPPORTED_REASONS, + ATTR_UPDATE_KEY, + ATTR_WS_EVENT, + DOMAIN, + EVENT_HEALTH_CHANGED, + EVENT_SUPERVISOR_EVENT, + EVENT_SUPERVISOR_UPDATE, + EVENT_SUPPORTED_CHANGED, + UPDATE_KEY_SUPERVISOR, +) +from .handler import HassIO + +ISSUE_ID_UNHEALTHY = "unhealthy_system" +ISSUE_ID_UNSUPPORTED = "unsupported_system" + +INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy" +INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported" + + +class SupervisorRepairs: + """Create repairs from supervisor events.""" + + def __init__(self, hass: HomeAssistant, client: HassIO) -> None: + """Initialize supervisor repairs.""" + self._hass = hass + self._client = client + self._unsupported_reasons: set[str] = set() + self._unhealthy_reasons: set[str] = set() + + @property + def unhealthy_reasons(self) -> set[str]: + """Get unhealthy reasons. Returns empty set if system is healthy.""" + return self._unhealthy_reasons + + @unhealthy_reasons.setter + def unhealthy_reasons(self, reasons: set[str]) -> None: + """Set unhealthy reasons. Create or delete repairs as necessary.""" + for unhealthy in reasons - self.unhealthy_reasons: + async_create_issue( + self._hass, + DOMAIN, + f"{ISSUE_ID_UNHEALTHY}_{unhealthy}", + is_fixable=False, + learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}", + severity=IssueSeverity.CRITICAL, + translation_key="unhealthy", + translation_placeholders={"reason": unhealthy}, + ) + + for fixed in self.unhealthy_reasons - reasons: + async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNHEALTHY}_{fixed}") + + self._unhealthy_reasons = reasons + + @property + def unsupported_reasons(self) -> set[str]: + """Get unsupported reasons. Returns empty set if system is supported.""" + return self._unsupported_reasons + + @unsupported_reasons.setter + def unsupported_reasons(self, reasons: set[str]) -> None: + """Set unsupported reasons. Create or delete repairs as necessary.""" + for unsupported in reasons - self.unsupported_reasons: + async_create_issue( + self._hass, + DOMAIN, + f"{ISSUE_ID_UNSUPPORTED}_{unsupported}", + is_fixable=False, + learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}", + severity=IssueSeverity.WARNING, + translation_key="unsupported", + translation_placeholders={"reason": unsupported}, + ) + + for fixed in self.unsupported_reasons - reasons: + async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}") + + self._unsupported_reasons = reasons + + async def setup(self) -> None: + """Create supervisor events listener.""" + await self.update() + + async_dispatcher_connect( + self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_repairs + ) + + async def update(self) -> None: + """Update repairs from Supervisor resolution center.""" + data = await self._client.get_resolution_info() + self.unhealthy_reasons = set(data[ATTR_UNHEALTHY]) + self.unsupported_reasons = set(data[ATTR_UNSUPPORTED]) + + @callback + def _supervisor_events_to_repairs(self, event: dict[str, Any]) -> None: + """Create repairs from supervisor events.""" + if ATTR_WS_EVENT not in event: + return + + if ( + event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE + and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR + ): + self._hass.async_create_task(self.update()) + + elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED: + self.unhealthy_reasons = ( + set() + if event[ATTR_DATA][ATTR_HEALTHY] + else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS]) + ) + + elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED: + self.unsupported_reasons = ( + set() + if event[ATTR_DATA][ATTR_SUPPORTED] + else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS]) + ) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 90142bd453f0b9..81b5ce01b79f57 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -15,5 +15,15 @@ "update_channel": "Update Channel", "version_api": "Version API" } + }, + "issues": { + "unhealthy": { + "title": "Unhealthy system - {reason}", + "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it." + }, + "unsupported": { + "title": "Unsupported system - {reason}", + "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system." + } } } diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index a601f98f1c5fd5..c2dab178ad83bb 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -133,6 +133,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 1f915e17e616a0..9eaaf5f97d9913 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_diagnostics( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f0f94661d50805..371398e32c958a 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -183,6 +183,19 @@ def mock_all(aioclient_mock, request, os_info): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_setup_api_ping(hass, aioclient_mock): @@ -191,7 +204,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -230,7 +243,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -246,7 +259,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -258,7 +271,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -325,7 +338,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -339,7 +352,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -356,7 +369,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -426,14 +439,14 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][2] == "test" await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_reboot", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count == 11 + assert aioclient_mock.call_count == 12 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -448,7 +461,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 13 + assert aioclient_mock.call_count == 14 assert aioclient_mock.mock_calls[-1][2] == { "homeassistant": True, "addons": ["test"], @@ -472,7 +485,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], @@ -491,12 +504,12 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): await hass.services.async_call("homeassistant", "stop") await hass.async_block_till_done() - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 await hass.services.async_call("homeassistant", "check_config") await hass.async_block_till_done() - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 with patch( "homeassistant.config.async_check_ha_config_file", return_value=None @@ -505,7 +518,7 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): await hass.async_block_till_done() assert mock_check_config.called - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 async def test_entry_load_and_unload(hass): @@ -758,7 +771,7 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): assert result await hass.async_block_till_done() - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py new file mode 100644 index 00000000000000..ebaf46be3b597a --- /dev/null +++ b/tests/components/hassio/test_repairs.py @@ -0,0 +1,395 @@ +"""Test repairs from supervisor issues.""" + +from __future__ import annotations + +import os +from typing import Any +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .test_init import MOCK_ENVIRON + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture(autouse=True) +async def setup_repairs(hass): + """Set up the repairs integration.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock: AiohttpClientMocker, request: pytest.FixtureRequest): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": { + "supervisor": "222", + "homeassistant": "0.110.0", + "hassos": "1.2.3", + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": { + "version_latest": "1.0.0", + "version": "1.0.0", + "update_available": False, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "version": "1.0.0", + "version_latest": "1.0.0", + "auto_update": True, + "addons": [], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + + +@pytest.fixture(autouse=True) +async def fixture_supervisor_environ(): + """Mock os environ for supervisor.""" + with patch.dict(os.environ, MOCK_ENVIRON): + yield + + +def mock_resolution_info( + aioclient_mock: AiohttpClientMocker, + unsupported: list[str] | None = None, + unhealthy: list[str] | None = None, +): + """Mock resolution/info endpoint with unsupported/unhealthy reasons.""" + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": unsupported or [], + "unhealthy": unhealthy or [], + "suggestions": [], + "issues": [], + "checks": [ + {"enabled": True, "slug": "supervisor_trust"}, + {"enabled": True, "slug": "free_space"}, + ], + }, + }, + ) + + +def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: str): + """Assert repair for unhealthy/unsupported in list.""" + repair_type = "unhealthy" if unhealthy else "unsupported" + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": f"{repair_type}_system_{reason}", + "issue_domain": None, + "learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}", + "severity": "critical" if unhealthy else "warning", + "translation_key": repair_type, + "translation_placeholders": { + "reason": reason, + }, + } in issues + + +async def test_unhealthy_repairs( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test repairs added for unhealthy systems.""" + mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup") + + +async def test_unsupported_repairs( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test repairs added for unsupported systems.""" + mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list( + msg["result"]["issues"], unhealthy=False, reason="content_trust" + ) + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + +async def test_unhealthy_repairs_add_remove( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test unhealthy repairs added and removed from dispatches.""" + mock_resolution_info(aioclient_mock) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "supervisor/event", + "data": { + "event": "health_changed", + "data": { + "healthy": False, + "unhealthy_reasons": ["docker"], + }, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + + await client.send_json( + { + "id": 3, + "type": "supervisor/event", + "data": { + "event": "health_changed", + "data": {"healthy": True}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_unsupported_repairs_add_remove( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test unsupported repairs added and removed from dispatches.""" + mock_resolution_info(aioclient_mock) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "supervisor/event", + "data": { + "event": "supported_changed", + "data": { + "supported": False, + "unsupported_reasons": ["os"], + }, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + await client.send_json( + { + "id": 3, + "type": "supervisor/event", + "data": { + "event": "supported_changed", + "data": {"supported": True}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_reset_repairs_supervisor_restart( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Unsupported/unhealthy repairs reset on supervisor restart.""" + mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + aioclient_mock.clear_requests() + mock_resolution_info(aioclient_mock) + await client.send_json( + { + "id": 2, + "type": "supervisor/event", + "data": { + "event": "supervisor_update", + "update_key": "supervisor", + "data": {}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_reasons_added_and_removed( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test an unsupported/unhealthy reasons being added and removed at same time.""" + mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + aioclient_mock.clear_requests() + mock_resolution_info( + aioclient_mock, unsupported=["content_trust"], unhealthy=["setup"] + ) + await client.send_json( + { + "id": 2, + "type": "supervisor/event", + "data": { + "event": "supervisor_update", + "update_key": "supervisor", + "data": {}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup") + assert_repair_in_list( + msg["result"]["issues"], unhealthy=False, reason="content_trust" + ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 16cce09b800a76..e9f0bd631b0679 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -126,6 +126,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index aaa77cde129d9e..02d6b1dbf6bc4a 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5d11d13166e58b..767f0abaf35ea0 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -61,6 +61,19 @@ def mock_all(aioclient_mock): aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 7a4202c1a674cb..a4249a1efb618b 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -198,7 +198,17 @@ async def unauth_handler(request): manager: IpBanManager = app[KEY_BAN_MANAGER] - assert await async_setup_component(hass, "hassio", {"hassio": {}}) + with patch( + "homeassistant.components.hassio.HassIO.get_resolution_info", + return_value={ + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + ): + assert await async_setup_component(hass, "hassio", {"hassio": {}}) m_open = mock_open() diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 204eb6bf77291b..40d889185ddbaf 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -57,6 +57,19 @@ async def mock_supervisor_fixture(hass, aioclient_mock): """Mock supervisor.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", return_value=True, From a0ed91e30c0d4f3db1d1961fd6433328c64002b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:30:29 +0100 Subject: [PATCH 097/394] Add type hints to rest tests (#81304) --- tests/components/rest/test_binary_sensor.py | 35 ++++----- tests/components/rest/test_init.py | 13 ++-- tests/components/rest/test_notify.py | 3 +- tests/components/rest/test_sensor.py | 75 ++++++++++++-------- tests/components/rest/test_switch.py | 78 +++++++++++++++------ 5 files changed, 130 insertions(+), 74 deletions(-) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index a6655f6ddbc250..757c331529eff8 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch import httpx +import pytest import respx from homeassistant import config as hass_config @@ -26,7 +27,7 @@ from tests.common import get_fixture_path -async def test_setup_missing_basic_config(hass): +async def test_setup_missing_basic_config(hass: HomeAssistant) -> None: """Test setup with configuration missing required entries.""" assert await async_setup_component( hass, Platform.BINARY_SENSOR, {"binary_sensor": {"platform": "rest"}} @@ -35,7 +36,7 @@ async def test_setup_missing_basic_config(hass): assert len(hass.states.async_all("binary_sensor")) == 0 -async def test_setup_missing_config(hass): +async def test_setup_missing_config(hass: HomeAssistant) -> None: """Test setup with configuration missing required entries.""" assert await async_setup_component( hass, @@ -53,7 +54,9 @@ async def test_setup_missing_config(hass): @respx.mock -async def test_setup_failed_connect(hass, caplog): +async def test_setup_failed_connect( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup when connection error occurs.""" respx.get("http://localhost").mock( @@ -76,7 +79,7 @@ async def test_setup_failed_connect(hass, caplog): @respx.mock -async def test_setup_timeout(hass): +async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs.""" respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( @@ -95,7 +98,7 @@ async def test_setup_timeout(hass): @respx.mock -async def test_setup_minimum(hass): +async def test_setup_minimum(hass: HomeAssistant) -> None: """Test setup with minimum configuration.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -114,7 +117,7 @@ async def test_setup_minimum(hass): @respx.mock -async def test_setup_minimum_resource_template(hass): +async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: """Test setup with minimum configuration (resource_template).""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -132,7 +135,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock -async def test_setup_duplicate_resource_template(hass): +async def test_setup_duplicate_resource_template(hass: HomeAssistant) -> None: """Test setup with duplicate resources.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -151,7 +154,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock -async def test_setup_get(hass): +async def test_setup_get(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -184,7 +187,7 @@ async def test_setup_get(hass): @respx.mock -async def test_setup_get_template_headers_params(hass): +async def test_setup_get_template_headers_params(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( @@ -218,7 +221,7 @@ async def test_setup_get_template_headers_params(hass): @respx.mock -async def test_setup_get_digest_auth(hass): +async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -246,7 +249,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock -async def test_setup_post(hass): +async def test_setup_post(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -274,7 +277,7 @@ async def test_setup_post(hass): @respx.mock -async def test_setup_get_off(hass): +async def test_setup_get_off(hass: HomeAssistant) -> None: """Test setup with valid off configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, @@ -304,7 +307,7 @@ async def test_setup_get_off(hass): @respx.mock -async def test_setup_get_on(hass): +async def test_setup_get_on(hass: HomeAssistant) -> None: """Test setup with valid on configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, @@ -334,7 +337,7 @@ async def test_setup_get_on(hass): @respx.mock -async def test_setup_with_exception(hass): +async def test_setup_with_exception(hass: HomeAssistant) -> None: """Test setup with exception.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -376,7 +379,7 @@ async def test_setup_with_exception(hass): @respx.mock -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload reset sensors.""" respx.get("http://localhost") % HTTPStatus.OK @@ -416,7 +419,7 @@ async def test_reload(hass): @respx.mock -async def test_setup_query_params(hass): +async def test_setup_query_params(hass: HomeAssistant) -> None: """Test setup with query params.""" respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 988f88b348e8e1..6dd2650c25c2c9 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -15,6 +15,7 @@ SERVICE_RELOAD, STATE_UNAVAILABLE, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -22,7 +23,7 @@ @respx.mock -async def test_setup_with_endpoint_timeout_with_recovery(hass): +async def test_setup_with_endpoint_timeout_with_recovery(hass: HomeAssistant) -> None: """Test setup with an endpoint that times out that recovers.""" await async_setup_component(hass, "homeassistant", {}) @@ -129,7 +130,7 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass): @respx.mock -async def test_setup_minimum_resource_template(hass): +async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: """Test setup with minimum configuration (resource_template).""" respx.get("http://localhost").respond( @@ -187,7 +188,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload.""" respx.get("http://localhost") % HTTPStatus.OK @@ -236,7 +237,7 @@ async def test_reload(hass): @respx.mock -async def test_reload_and_remove_all(hass): +async def test_reload_and_remove_all(hass: HomeAssistant) -> None: """Verify we can reload and remove all.""" respx.get("http://localhost") % HTTPStatus.OK @@ -283,7 +284,7 @@ async def test_reload_and_remove_all(hass): @respx.mock -async def test_reload_fails_to_read_configuration(hass): +async def test_reload_fails_to_read_configuration(hass: HomeAssistant) -> None: """Verify reload when configuration is missing or broken.""" respx.get("http://localhost") % HTTPStatus.OK @@ -327,7 +328,7 @@ async def test_reload_fails_to_read_configuration(hass): @respx.mock -async def test_multiple_rest_endpoints(hass): +async def test_multiple_rest_endpoints(hass: HomeAssistant) -> None: """Test multiple rest endpoints.""" respx.get("http://date.jsontest.com").respond( diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index 31567ae63f0c90..f9a2e88c7327ce 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -7,13 +7,14 @@ import homeassistant.components.notify as notify from homeassistant.components.rest import DOMAIN from homeassistant.const import SERVICE_RELOAD +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @respx.mock -async def test_reload_notify(hass): +async def test_reload_notify(hass: HomeAssistant) -> None: """Verify we can reload the notify service.""" respx.get("http://localhost") % 200 diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index a89d20f2510de3..49ad69b1caa61b 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch import httpx +import pytest import respx from homeassistant import config as hass_config @@ -31,14 +32,14 @@ from tests.common import get_fixture_path -async def test_setup_missing_config(hass): +async def test_setup_missing_config(hass: HomeAssistant) -> None: """Test setup with configuration missing required entries.""" assert await async_setup_component(hass, DOMAIN, {"sensor": {"platform": "rest"}}) await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 0 -async def test_setup_missing_schema(hass): +async def test_setup_missing_schema(hass: HomeAssistant) -> None: """Test setup with resource missing schema.""" assert await async_setup_component( hass, @@ -50,7 +51,9 @@ async def test_setup_missing_schema(hass): @respx.mock -async def test_setup_failed_connect(hass, caplog): +async def test_setup_failed_connect( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup when connection error occurs.""" respx.get("http://localhost").mock( side_effect=httpx.RequestError("server offline", request=MagicMock()) @@ -72,7 +75,7 @@ async def test_setup_failed_connect(hass, caplog): @respx.mock -async def test_setup_timeout(hass): +async def test_setup_timeout(hass: HomeAssistant) -> None: """Test setup when connection timeout occurs.""" respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( @@ -85,7 +88,7 @@ async def test_setup_timeout(hass): @respx.mock -async def test_setup_minimum(hass): +async def test_setup_minimum(hass: HomeAssistant) -> None: """Test setup with minimum configuration.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -104,7 +107,7 @@ async def test_setup_minimum(hass): @respx.mock -async def test_manual_update(hass): +async def test_manual_update(hass: HomeAssistant) -> None: """Test setup with minimum configuration.""" await async_setup_component(hass, "homeassistant", {}) respx.get("http://localhost").respond( @@ -140,7 +143,7 @@ async def test_manual_update(hass): @respx.mock -async def test_setup_minimum_resource_template(hass): +async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: """Test setup with minimum configuration (resource_template).""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -158,7 +161,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock -async def test_setup_duplicate_resource_template(hass): +async def test_setup_duplicate_resource_template(hass: HomeAssistant) -> None: """Test setup with duplicate resources.""" respx.get("http://localhost") % HTTPStatus.OK assert await async_setup_component( @@ -177,7 +180,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock -async def test_setup_get(hass): +async def test_setup_get(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -223,7 +226,9 @@ async def test_setup_get(hass): @respx.mock -async def test_setup_timestamp(hass, caplog): +async def test_setup_timestamp( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, json={"key": "2021-11-11 11:39Z"} @@ -286,7 +291,7 @@ async def test_setup_timestamp(hass, caplog): @respx.mock -async def test_setup_get_templated_headers_params(hass): +async def test_setup_get_templated_headers_params(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( @@ -320,7 +325,7 @@ async def test_setup_get_templated_headers_params(hass): @respx.mock -async def test_setup_get_digest_auth(hass): +async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -349,7 +354,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock -async def test_setup_post(hass): +async def test_setup_post(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) assert await async_setup_component( @@ -378,7 +383,7 @@ async def test_setup_post(hass): @respx.mock -async def test_setup_get_xml(hass): +async def test_setup_get_xml(hass: HomeAssistant) -> None: """Test setup with valid xml configuration.""" respx.get("http://localhost").respond( status_code=HTTPStatus.OK, @@ -410,7 +415,7 @@ async def test_setup_get_xml(hass): @respx.mock -async def test_setup_query_params(hass): +async def test_setup_query_params(hass: HomeAssistant) -> None: """Test setup with query params.""" respx.get("http://localhost", params={"search": "something"}) % HTTPStatus.OK assert await async_setup_component( @@ -430,7 +435,7 @@ async def test_setup_query_params(hass): @respx.mock -async def test_update_with_json_attrs(hass): +async def test_update_with_json_attrs(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( @@ -463,7 +468,7 @@ async def test_update_with_json_attrs(hass): @respx.mock -async def test_update_with_no_template(hass): +async def test_update_with_no_template(hass: HomeAssistant) -> None: """Test update when there is no value template.""" respx.get("http://localhost").respond( @@ -495,7 +500,9 @@ async def test_update_with_no_template(hass): @respx.mock -async def test_update_with_json_attrs_no_data(hass, caplog): +async def test_update_with_json_attrs_no_data( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes when no JSON result fetched.""" respx.get("http://localhost").respond( @@ -531,7 +538,9 @@ async def test_update_with_json_attrs_no_data(hass, caplog): @respx.mock -async def test_update_with_json_attrs_not_dict(hass, caplog): +async def test_update_with_json_attrs_not_dict( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( @@ -566,7 +575,9 @@ async def test_update_with_json_attrs_not_dict(hass, caplog): @respx.mock -async def test_update_with_json_attrs_bad_JSON(hass, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a JSON result.""" respx.get("http://localhost").respond( @@ -602,7 +613,7 @@ async def test_update_with_json_attrs_bad_JSON(hass, caplog): @respx.mock -async def test_update_with_json_attrs_with_json_attrs_path(hass): +async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result with a template for the attributes.""" respx.get("http://localhost").respond( @@ -646,7 +657,9 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass): @respx.mock -async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): +async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( + hass: HomeAssistant, +) -> None: """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes.""" respx.get("http://localhost").respond( @@ -682,7 +695,9 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): @respx.mock -async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): +async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( + hass: HomeAssistant, +) -> None: """Test attributes get extracted from a JSON result that was converted from XML.""" respx.get("http://localhost").respond( @@ -722,8 +737,8 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): @respx.mock async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_template( - hass, -): + hass: HomeAssistant, +) -> None: """Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type.""" respx.get("http://localhost").respond( @@ -759,7 +774,9 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp @respx.mock -async def test_update_with_xml_convert_bad_xml(hass, caplog): +async def test_update_with_xml_convert_bad_xml( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a XML result with bad xml.""" respx.get("http://localhost").respond( @@ -794,7 +811,9 @@ async def test_update_with_xml_convert_bad_xml(hass, caplog): @respx.mock -async def test_update_with_failed_get(hass, caplog): +async def test_update_with_failed_get( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test attributes get extracted from a XML result with bad xml.""" respx.get("http://localhost").respond( @@ -829,7 +848,7 @@ async def test_update_with_failed_get(hass, caplog): @respx.mock -async def test_reload(hass): +async def test_reload(hass: HomeAssistant) -> None: """Verify we can reload reset sensors.""" respx.get("http://localhost") % HTTPStatus.OK diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index a3c0f78db1c8ea..6275314bcf0414 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -33,12 +33,12 @@ PARAMS = None -async def test_setup_missing_config(hass): +async def test_setup_missing_config(hass: HomeAssistant) -> None: """Test setup with configuration missing required entries.""" assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None) -async def test_setup_missing_schema(hass): +async def test_setup_missing_schema(hass: HomeAssistant) -> None: """Test setup with resource missing schema.""" assert not await rest.async_setup_platform( hass, @@ -47,7 +47,9 @@ async def test_setup_missing_schema(hass): ) -async def test_setup_failed_connect(hass, aioclient_mock): +async def test_setup_failed_connect( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup when connection error occurs.""" aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) assert not await rest.async_setup_platform( @@ -57,7 +59,9 @@ async def test_setup_failed_connect(hass, aioclient_mock): ) -async def test_setup_timeout(hass, aioclient_mock): +async def test_setup_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup when connection timeout occurs.""" aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) assert not await rest.async_setup_platform( @@ -67,7 +71,9 @@ async def test_setup_timeout(hass, aioclient_mock): ) -async def test_setup_minimum(hass, aioclient_mock): +async def test_setup_minimum( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with minimum configuration.""" aioclient_mock.get("http://localhost", status=HTTPStatus.OK) with assert_setup_component(1, Platform.SWITCH): @@ -85,7 +91,9 @@ async def test_setup_minimum(hass, aioclient_mock): assert aioclient_mock.call_count == 1 -async def test_setup_query_params(hass, aioclient_mock): +async def test_setup_query_params( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with query params.""" aioclient_mock.get("http://localhost/?search=something", status=HTTPStatus.OK) with assert_setup_component(1, Platform.SWITCH): @@ -105,7 +113,7 @@ async def test_setup_query_params(hass, aioclient_mock): assert aioclient_mock.call_count == 1 -async def test_setup(hass, aioclient_mock): +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with valid configuration.""" aioclient_mock.get("http://localhost", status=HTTPStatus.OK) assert await async_setup_component( @@ -127,7 +135,9 @@ async def test_setup(hass, aioclient_mock): assert_setup_component(1, Platform.SWITCH) -async def test_setup_with_state_resource(hass, aioclient_mock): +async def test_setup_with_state_resource( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with valid configuration.""" aioclient_mock.get("http://localhost", status=HTTPStatus.NOT_FOUND) aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK) @@ -151,7 +161,9 @@ async def test_setup_with_state_resource(hass, aioclient_mock): assert_setup_component(1, Platform.SWITCH) -async def test_setup_with_templated_headers_params(hass, aioclient_mock): +async def test_setup_with_templated_headers_params( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with valid configuration.""" aioclient_mock.get("http://localhost", status=HTTPStatus.OK) assert await async_setup_component( @@ -185,7 +197,7 @@ async def test_setup_with_templated_headers_params(hass, aioclient_mock): """Tests for REST switch platform.""" -def _setup_test_switch(hass): +def _setup_test_switch(hass: HomeAssistant) -> None: body_on = Template("on", hass) body_off = Template("off", hass) headers = {"Content-type": Template(CONTENT_TYPE_JSON, hass)} @@ -211,25 +223,27 @@ def _setup_test_switch(hass): return switch, body_on, body_off -def test_name(hass): +def test_name(hass: HomeAssistant) -> None: """Test the name.""" switch, body_on, body_off = _setup_test_switch(hass) assert switch.name == NAME -def test_device_class(hass): +def test_device_class(hass: HomeAssistant) -> None: """Test the name.""" switch, body_on, body_off = _setup_test_switch(hass) assert switch.device_class == DEVICE_CLASS -def test_is_on_before_update(hass): +def test_is_on_before_update(hass: HomeAssistant) -> None: """Test is_on in initial state.""" switch, body_on, body_off = _setup_test_switch(hass) assert switch.is_on is None -async def test_turn_on_success(hass, aioclient_mock): +async def test_turn_on_success( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_on.""" aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) switch, body_on, body_off = _setup_test_switch(hass) @@ -239,7 +253,9 @@ async def test_turn_on_success(hass, aioclient_mock): assert switch.is_on -async def test_turn_on_status_not_ok(hass, aioclient_mock): +async def test_turn_on_status_not_ok( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_on when error status returned.""" aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) switch, body_on, body_off = _setup_test_switch(hass) @@ -249,7 +265,9 @@ async def test_turn_on_status_not_ok(hass, aioclient_mock): assert switch.is_on is None -async def test_turn_on_timeout(hass, aioclient_mock): +async def test_turn_on_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_on when timeout occurs.""" aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) switch, body_on, body_off = _setup_test_switch(hass) @@ -258,7 +276,9 @@ async def test_turn_on_timeout(hass, aioclient_mock): assert switch.is_on is None -async def test_turn_off_success(hass, aioclient_mock): +async def test_turn_off_success( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_off.""" aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) switch, body_on, body_off = _setup_test_switch(hass) @@ -268,7 +288,9 @@ async def test_turn_off_success(hass, aioclient_mock): assert not switch.is_on -async def test_turn_off_status_not_ok(hass, aioclient_mock): +async def test_turn_off_status_not_ok( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_off when error status returned.""" aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) switch, body_on, body_off = _setup_test_switch(hass) @@ -278,7 +300,9 @@ async def test_turn_off_status_not_ok(hass, aioclient_mock): assert switch.is_on is None -async def test_turn_off_timeout(hass, aioclient_mock): +async def test_turn_off_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test turn_off when timeout occurs.""" aioclient_mock.post(RESOURCE, exc=asyncio.TimeoutError()) switch, body_on, body_off = _setup_test_switch(hass) @@ -287,7 +311,9 @@ async def test_turn_off_timeout(hass, aioclient_mock): assert switch.is_on is None -async def test_update_when_on(hass, aioclient_mock): +async def test_update_when_on( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when switch is on.""" switch, body_on, body_off = _setup_test_switch(hass) aioclient_mock.get(RESOURCE, text=body_on.template) @@ -296,7 +322,9 @@ async def test_update_when_on(hass, aioclient_mock): assert switch.is_on -async def test_update_when_off(hass, aioclient_mock): +async def test_update_when_off( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when switch is off.""" switch, body_on, body_off = _setup_test_switch(hass) aioclient_mock.get(RESOURCE, text=body_off.template) @@ -305,7 +333,9 @@ async def test_update_when_off(hass, aioclient_mock): assert not switch.is_on -async def test_update_when_unknown(hass, aioclient_mock): +async def test_update_when_unknown( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when unknown status returned.""" aioclient_mock.get(RESOURCE, text="unknown status") switch, body_on, body_off = _setup_test_switch(hass) @@ -314,7 +344,9 @@ async def test_update_when_unknown(hass, aioclient_mock): assert switch.is_on is None -async def test_update_timeout(hass, aioclient_mock): +async def test_update_timeout( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test update when timeout occurs.""" aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) switch, body_on, body_off = _setup_test_switch(hass) From fee3898f648d4fffdf9dbec748aab2410a0bd227 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:36:43 +0100 Subject: [PATCH 098/394] Use _attr_is_on in rest (#81305) --- .../components/rest/binary_sensor.py | 20 +++++++--------- homeassistant/components/rest/switch.py | 23 +++++++------------ 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index bc51433c3c5d19..fc40d76a21d7f3 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -100,24 +100,17 @@ def __init__( fallback_name=DEFAULT_BINARY_SENSOR_NAME, unique_id=unique_id, ) - self._state = False self._previous_data = None self._value_template = config.get(CONF_VALUE_TEMPLATE) if (value_template := self._value_template) is not None: value_template.hass = hass - self._is_on = None self._attr_device_class = config.get(CONF_DEVICE_CLASS) - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._is_on - def _update_from_rest_data(self): """Update state from the rest data.""" if self.rest.data is None: - self._is_on = False + self._attr_is_on = False response = self.rest.data @@ -127,8 +120,11 @@ def _update_from_rest_data(self): ) try: - self._is_on = bool(int(response)) + self._attr_is_on = bool(int(response)) except ValueError: - self._is_on = {"true": True, "on": True, "open": True, "yes": True}.get( - response.lower(), False - ) + self._attr_is_on = { + "true": True, + "on": True, + "open": True, + "yes": True, + }.get(response.lower(), False) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index f2a5d93cd22830..7e470674b1ee5d 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -119,8 +119,6 @@ def __init__( unique_id=unique_id, ) - self._state = None - auth = None if username := config.get(CONF_USERNAME): auth = aiohttp.BasicAuth(username, password=config[CONF_PASSWORD]) @@ -149,11 +147,6 @@ def __init__( template.attach(hass, self._headers) template.attach(hass, self._params) - @property - def is_on(self): - """Return true if device is on.""" - return self._state - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" body_on_t = self._body_on.async_render(parse_result=False) @@ -162,7 +155,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: req = await self.set_device_state(body_on_t) if req.status == HTTPStatus.OK: - self._state = True + self._attr_is_on = True else: _LOGGER.error( "Can't turn on %s. Is resource/endpoint offline?", self._resource @@ -177,7 +170,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: try: req = await self.set_device_state(body_off_t) if req.status == HTTPStatus.OK: - self._state = False + self._attr_is_on = False else: _LOGGER.error( "Can't turn off %s. Is resource/endpoint offline?", self._resource @@ -233,17 +226,17 @@ async def get_device_state(self, hass): ) text = text.lower() if text == "true": - self._state = True + self._attr_is_on = True elif text == "false": - self._state = False + self._attr_is_on = False else: - self._state = None + self._attr_is_on = None else: if text == self._body_on.template: - self._state = True + self._attr_is_on = True elif text == self._body_off.template: - self._state = False + self._attr_is_on = False else: - self._state = None + self._attr_is_on = None return req From 94e2646c875a539f2c48d40eacf24f93f2e69026 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:56:13 +0100 Subject: [PATCH 099/394] Implement reauth_confirm in fireservicerota (#77487) --- .../components/fireservicerota/config_flow.py | 30 +++++++++++-------- .../components/fireservicerota/strings.json | 2 +- .../fireservicerota/translations/en.json | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index 3fcd3870f6a52f..d4d2b0763d9958 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -1,9 +1,15 @@ """Config flow for FireServiceRota.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + from pyfireservicerota import FireServiceRota, InvalidAuthError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, URL_LIST @@ -110,18 +116,18 @@ def _show_setup_form(self, user_input=None, errors=None, step_id="user"): description_placeholders=self._description_placeholders, ) - async def async_step_reauth(self, user_input=None): - """Get new tokens for a config entry that can't authenticate.""" - - if not self._existing_entry: - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._existing_entry = user_input.copy() - self._description_placeholders = {"username": user_input[CONF_USERNAME]} - user_input = None + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Initialise re-authentication.""" + await self.async_set_unique_id(entry_data[CONF_USERNAME]) + self._existing_entry = {**entry_data} + self._description_placeholders = {CONF_USERNAME: entry_data[CONF_USERNAME]} + return await self.async_step_reauth_confirm() + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Get new tokens for a config entry that can't authenticate.""" if user_input is None: - return self._show_setup_form(step_id=config_entries.SOURCE_REAUTH) + return self._show_setup_form(step_id="reauth_confirm") - return await self._validate_and_create_entry( - user_input, config_entries.SOURCE_REAUTH - ) + return await self._validate_and_create_entry(user_input, "reauth_confirm") diff --git a/homeassistant/components/fireservicerota/strings.json b/homeassistant/components/fireservicerota/strings.json index aef6f1b684918b..7c60b4382641e2 100644 --- a/homeassistant/components/fireservicerota/strings.json +++ b/homeassistant/components/fireservicerota/strings.json @@ -8,7 +8,7 @@ "url": "Website" } }, - "reauth": { + "reauth_confirm": { "description": "Authentication tokens became invalid, login to recreate them.", "data": { "password": "[%key:common::config_flow::data::password%]" diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json index a059081760dec3..38762b614f4268 100644 --- a/homeassistant/components/fireservicerota/translations/en.json +++ b/homeassistant/components/fireservicerota/translations/en.json @@ -11,7 +11,7 @@ "invalid_auth": "Invalid authentication" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "password": "Password" }, From 4f5aad9d6d450f1dd9786f126057e0028d4a9c3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 12:29:12 -0500 Subject: [PATCH 100/394] Bump aiohomekit to 2.2.10 (#81312) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 58e258294a02ec..93aae62daab91e 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.9"], + "requirements": ["aiohomekit==2.2.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index f3db2448dd06dc..d4176e4d471b8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.9 +aiohomekit==2.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45b8e84298a9cf..591b48704f57ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.9 +aiohomekit==2.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http From 3764f7d95bd1a88cf8b04d5808f506372150852c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 12:35:43 -0500 Subject: [PATCH 101/394] Bump zeroconf to 0.39.4 (#81313) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 967dd761ac7977..382cf42b54fde7 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.39.3"], + "requirements": ["zeroconf==0.39.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e2c57d87c19c03..411b0a06646a27 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.8.1 -zeroconf==0.39.3 +zeroconf==0.39.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index d4176e4d471b8b..602b391e0e18d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2607,7 +2607,7 @@ zamg==0.1.1 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.3 +zeroconf==0.39.4 # homeassistant.components.zha zha-quirks==0.0.84 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 591b48704f57ac..4c68a58dff95f2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1808,7 +1808,7 @@ youless-api==0.16 zamg==0.1.1 # homeassistant.components.zeroconf -zeroconf==0.39.3 +zeroconf==0.39.4 # homeassistant.components.zha zha-quirks==0.0.84 From 82e90587c7be757d8ede9c210b9fa04a3d3f4a97 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 13:38:57 -0500 Subject: [PATCH 102/394] Bump oralb-ble to 0.10.0 (#81315) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 8f6949468048c0..cad6167228cce4 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.9.0"], + "requirements": ["oralb-ble==0.10.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 602b391e0e18d7..5915510eed891e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.9.0 +oralb-ble==0.10.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c68a58dff95f2..9094f51cc1ac80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.9.0 +oralb-ble==0.10.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From c08848b22ea4e9d19b41cc9fa5118c9585f812c8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Oct 2022 20:36:59 +0100 Subject: [PATCH 103/394] Update base image to 2022.10.0 (#81317) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 9cf66e2621a073..14a59641388ba2 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.10.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.10.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.10.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.10.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.10.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 8044b9587afc9bf2bb09c880f7b12c6cd6bc7664 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 31 Oct 2022 19:41:12 +0000 Subject: [PATCH 104/394] Add integration type to System Bridge (#81186) --- homeassistant/components/system_bridge/manifest.json | 1 + homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 9370de7078710f..f386ed57085b94 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -9,6 +9,7 @@ "dependencies": ["media_source"], "after_dependencies": ["zeroconf"], "quality_scale": "silver", + "integration_type": "device", "iot_class": "local_push", "loggers": ["systembridgeconnector"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 5e245ad9734804..3614f5d7afba67 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5151,7 +5151,7 @@ }, "system_bridge": { "name": "System Bridge", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_push" }, From d6689937a34d4ab0f30c36f7f9c5d0f20d8f424f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 31 Oct 2022 19:42:02 +0000 Subject: [PATCH 105/394] Add integration type to OVO Energy (#81187) --- homeassistant/components/ovo_energy/manifest.json | 1 + homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ovo_energy/manifest.json b/homeassistant/components/ovo_energy/manifest.json index 6d994e472c8560..e61aaebe190f0f 100644 --- a/homeassistant/components/ovo_energy/manifest.json +++ b/homeassistant/components/ovo_energy/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/ovo_energy", "requirements": ["ovoenergy==1.2.0"], "codeowners": ["@timmo001"], + "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["ovoenergy"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3614f5d7afba67..57452743af1e14 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3831,7 +3831,7 @@ }, "ovo_energy": { "name": "OVO Energy", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, From 4a9859bf54c7b255cc1c739c32f0a6bd197ffc4b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 31 Oct 2022 20:42:18 +0100 Subject: [PATCH 106/394] Update frontend to 20221031.0 (#81324) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c8d3645435f0d9..aed26eb5de1ea4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221027.0"], + "requirements": ["home-assistant-frontend==20221031.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 411b0a06646a27..adff342729de81 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.60.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5915510eed891e..5fb9331bd2928e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9094f51cc1ac80..ca71daa39be8f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -651,7 +651,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 # homeassistant.components.home_connect homeconnect==0.7.2 From f8de4c3931fbc3c4d9cfde3b9297b23a98571648 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 1 Nov 2022 00:01:22 +0200 Subject: [PATCH 107/394] Reauth flow for Risco cloud (#81264) * Risco reauth flow * Address code review comments * Remove redundant log --- homeassistant/components/risco/__init__.py | 9 ++- homeassistant/components/risco/config_flow.py | 26 ++++++- tests/components/risco/conftest.py | 5 +- tests/components/risco/test_config_flow.py | 70 +++++++++++++++++++ 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 0e631cc4a9325d..a9a462bf916a77 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -26,7 +26,7 @@ Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store @@ -127,10 +127,9 @@ async def _async_setup_cloud_entry(hass: HomeAssistant, entry: ConfigEntry) -> b try: await risco.login(async_get_clientsession(hass)) except CannotConnectError as error: - raise ConfigEntryNotReady() from error - except UnauthorizedError: - _LOGGER.exception("Failed to login to Risco cloud") - return False + raise ConfigEntryNotReady from error + except UnauthorizedError as error: + raise ConfigEntryAuthFailed from error scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) coordinator = RiscoDataUpdateCoordinator(hass, risco, scan_interval) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 5e1cdb75b5a977..91e12a2548ad9b 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -3,6 +3,7 @@ from collections.abc import Mapping import logging +from typing import Any from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError import voluptuous as vol @@ -21,6 +22,7 @@ STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -93,6 +95,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Init the config flow.""" + self._reauth_entry: config_entries.ConfigEntry | None = None + @staticmethod @core.callback def async_get_options_flow( @@ -112,8 +118,9 @@ async def async_step_cloud(self, user_input=None): """Configure a cloud based alarm.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() + if not self._reauth_entry: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() try: info = await validate_cloud_input(self.hass, user_input) @@ -125,12 +132,25 @@ async def async_step_cloud(self, user_input=None): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry(title=info["title"], data=user_input) + if not self._reauth_entry: + return self.async_create_entry(title=info["title"], data=user_input) + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data=user_input, + unique_id=user_input[CONF_USERNAME], + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") return self.async_show_form( step_id="cloud", data_schema=CLOUD_SCHEMA, errors=errors ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauth_entry = await self.async_set_unique_id(entry_data[CONF_USERNAME]) + return await self.async_step_cloud() + async def async_step_local(self, user_input=None): """Configure a local based alarm.""" errors = {} diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index 006e57b9ae5076..b22768a1cd0d3a 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -70,7 +70,10 @@ def events(): def cloud_config_entry(hass, options): """Fixture for a cloud config entry.""" config_entry = MockConfigEntry( - domain=DOMAIN, data=TEST_CLOUD_CONFIG, options=options + domain=DOMAIN, + data=TEST_CLOUD_CONFIG, + options=options, + unique_id=TEST_CLOUD_CONFIG[CONF_USERNAME], ) config_entry.add_to_hass(hass) return config_entry diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 396aad8015dd4e..0c71ba9efdc081 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -10,6 +10,7 @@ UnauthorizedError, ) from homeassistant.components.risco.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -142,6 +143,75 @@ async def test_form_cloud_already_exists(hass): assert result3["reason"] == "already_configured" +async def test_form_reauth(hass, cloud_config_entry): + """Test reauthenticate.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=cloud_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ), patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**TEST_CLOUD_DATA, CONF_PASSWORD: "new_password"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert cloud_config_entry.data[CONF_PASSWORD] == "new_password" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_reauth_with_new_username(hass, cloud_config_entry): + """Test reauthenticate with new username.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=cloud_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ), patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**TEST_CLOUD_DATA, CONF_USERNAME: "new_user"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert cloud_config_entry.data[CONF_USERNAME] == "new_user" + assert cloud_config_entry.unique_id == "new_user" + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_local_form(hass): """Test we get the local form.""" result = await hass.config_entries.flow.async_init( From 009d5aedd51e1aedfee5294cde4ece278b62bd8a Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 1 Nov 2022 01:29:00 +0200 Subject: [PATCH 108/394] Extract `bypassed` attribute in Risco zones to a switch (#81137) * Split bypassed to a switch * Address code review comments --- homeassistant/components/risco/__init__.py | 7 +- .../components/risco/alarm_control_panel.py | 4 +- .../components/risco/binary_sensor.py | 132 +++------------ homeassistant/components/risco/entity.py | 102 +++++++++++- homeassistant/components/risco/sensor.py | 8 +- homeassistant/components/risco/services.yaml | 17 -- homeassistant/components/risco/switch.py | 104 ++++++++++++ tests/components/risco/conftest.py | 34 ++++ tests/components/risco/test_binary_sensor.py | 132 ++------------- tests/components/risco/test_switch.py | 151 ++++++++++++++++++ 10 files changed, 430 insertions(+), 261 deletions(-) delete mode 100644 homeassistant/components/risco/services.yaml create mode 100644 homeassistant/components/risco/switch.py create mode 100644 tests/components/risco/test_switch.py diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index a9a462bf916a77..f143244d31dd8c 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -40,7 +40,12 @@ TYPE_LOCAL, ) -PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.SENSOR, + Platform.SWITCH, +] LAST_EVENT_STORAGE_VERSION = 1 LAST_EVENT_TIMESTAMP_KEY = "last_event_timestamp" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index 79da100d6e1184..7f4241048d7a5a 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -40,7 +40,7 @@ RISCO_GROUPS, RISCO_PARTIAL_ARM, ) -from .entity import RiscoEntity +from .entity import RiscoCloudEntity _LOGGER = logging.getLogger(__name__) @@ -178,7 +178,7 @@ async def _call_alarm_method(self, method: str, *args: Any) -> None: raise NotImplementedError -class RiscoCloudAlarm(RiscoAlarm, RiscoEntity): +class RiscoCloudAlarm(RiscoAlarm, RiscoCloudEntity): """Representation of a Risco partition.""" def __init__( diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index bc021c2c364c43..b1f55dd8693c94 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -12,21 +12,11 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_platform -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LocalData, RiscoDataUpdateCoordinator, is_local, zone_update_signal +from . import LocalData, RiscoDataUpdateCoordinator, is_local from .const import DATA_COORDINATOR, DOMAIN -from .entity import RiscoEntity, binary_sensor_unique_id - -SERVICE_BYPASS_ZONE = "bypass_zone" -SERVICE_UNBYPASS_ZONE = "unbypass_zone" - - -def _unique_id_for_local(system_id: str, zone_id: int) -> str: - return f"{system_id}_zone_{zone_id}_local" +from .entity import RiscoCloudZoneEntity, RiscoLocalZoneEntity async def async_setup_entry( @@ -35,12 +25,6 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Risco alarm control panel.""" - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service(SERVICE_BYPASS_ZONE, {}, "async_bypass_zone") - platform.async_register_entity_service( - SERVICE_UNBYPASS_ZONE, {}, "async_unbypass_zone" - ) - if is_local(config_entry): local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( @@ -61,85 +45,34 @@ async def async_setup_entry( ) -class RiscoBinarySensor(BinarySensorEntity): - """Representation of a Risco zone as a binary sensor.""" +class RiscoCloudBinarySensor(RiscoCloudZoneEntity, BinarySensorEntity): + """Representation of a Risco cloud zone as a binary sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION - def __init__(self, *, zone_id: int, zone: Zone, **kwargs: Any) -> None: - """Init the zone.""" - super().__init__(**kwargs) - self._zone_id = zone_id - self._zone = zone - self._attr_has_entity_name = True - self._attr_name = None - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return the state attributes.""" - return {"zone_id": self._zone_id, "bypassed": self._zone.bypassed} - - @property - def is_on(self) -> bool | None: - """Return true if sensor is on.""" - return self._zone.triggered - - async def async_bypass_zone(self) -> None: - """Bypass this zone.""" - await self._bypass(True) - - async def async_unbypass_zone(self) -> None: - """Unbypass this zone.""" - await self._bypass(False) - - async def _bypass(self, bypass: bool) -> None: - raise NotImplementedError - - -class RiscoCloudBinarySensor(RiscoBinarySensor, RiscoEntity): - """Representation of a Risco cloud zone as a binary sensor.""" - def __init__( self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone ) -> None: """Init the zone.""" - super().__init__(zone_id=zone_id, zone=zone, coordinator=coordinator) - self._attr_unique_id = binary_sensor_unique_id(self._risco, zone_id) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer="Risco", - name=self._zone.name, + super().__init__( + coordinator=coordinator, name=None, suffix="", zone_id=zone_id, zone=zone ) - def _get_data_from_coordinator(self) -> None: - self._zone = self.coordinator.data.zones[self._zone_id] - - async def _bypass(self, bypass: bool) -> None: - alarm = await self._risco.bypass_zone(self._zone_id, bypass) - self._zone = alarm.zones[self._zone_id] - self.async_write_ha_state() + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return self._zone.triggered -class RiscoLocalBinarySensor(RiscoBinarySensor): +class RiscoLocalBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation of a Risco local zone as a binary sensor.""" - _attr_should_poll = False + _attr_device_class = BinarySensorDeviceClass.MOTION def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" - super().__init__(zone_id=zone_id, zone=zone) - self._attr_unique_id = _unique_id_for_local(system_id, zone_id) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, - manufacturer="Risco", - name=self._zone.name, - ) - - async def async_added_to_hass(self) -> None: - """Subscribe to updates.""" - signal = zone_update_signal(self._zone_id) - self.async_on_remove( - async_dispatcher_connect(self.hass, signal, self.async_write_ha_state) + super().__init__( + system_id=system_id, name=None, suffix="", zone_id=zone_id, zone=zone ) @property @@ -150,42 +83,27 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: "groups": self._zone.groups, } - async def _bypass(self, bypass: bool) -> None: - await self._zone.bypass(bypass) + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return self._zone.triggered -class RiscoLocalAlarmedBinarySensor(BinarySensorEntity): +class RiscoLocalAlarmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation whether a zone in Risco local is currently triggering an alarm.""" _attr_should_poll = False def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" - super().__init__() - self._zone_id = zone_id - self._zone = zone - self._attr_has_entity_name = True - self._attr_name = "Alarmed" - device_unique_id = _unique_id_for_local(system_id, zone_id) - self._attr_unique_id = device_unique_id + "_alarmed" - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, device_unique_id)}, - manufacturer="Risco", - name=self._zone.name, - ) - - async def async_added_to_hass(self) -> None: - """Subscribe to updates.""" - signal = zone_update_signal(self._zone_id) - self.async_on_remove( - async_dispatcher_connect(self.hass, signal, self.async_write_ha_state) + super().__init__( + system_id=system_id, + name="Alarmed", + suffix="_alarmed", + zone_id=zone_id, + zone=zone, ) - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return the state attributes.""" - return {"zone_id": self._zone_id} - @property def is_on(self) -> bool | None: """Return true if sensor is on.""" diff --git a/homeassistant/components/risco/entity.py b/homeassistant/components/risco/entity.py index e49b632ac7838f..a4ac260887c28d 100644 --- a/homeassistant/components/risco/entity.py +++ b/homeassistant/components/risco/entity.py @@ -1,25 +1,40 @@ """A risco entity base class.""" +from __future__ import annotations + +from typing import Any + +from pyrisco.common import Zone + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import RiscoDataUpdateCoordinator +from . import RiscoDataUpdateCoordinator, zone_update_signal +from .const import DOMAIN -def binary_sensor_unique_id(risco, zone_id: int) -> str: - """Return unique id for the binary sensor.""" +def zone_unique_id(risco, zone_id: int) -> str: + """Return unique id for a cloud zone.""" return f"{risco.site_uuid}_zone_{zone_id}" -class RiscoEntity(CoordinatorEntity[RiscoDataUpdateCoordinator]): - """Risco entity base class.""" +class RiscoCloudEntity(CoordinatorEntity[RiscoDataUpdateCoordinator]): + """Risco cloud entity base class.""" - def _get_data_from_coordinator(self): + def __init__( + self, *, coordinator: RiscoDataUpdateCoordinator, **kwargs: Any + ) -> None: + """Init the entity.""" + super().__init__(coordinator=coordinator, **kwargs) + + def _get_data_from_coordinator(self) -> None: raise NotImplementedError - def _refresh_from_coordinator(self): + def _refresh_from_coordinator(self) -> None: self._get_data_from_coordinator() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( self.coordinator.async_add_listener(self._refresh_from_coordinator) @@ -29,3 +44,74 @@ async def async_added_to_hass(self): def _risco(self): """Return the Risco API object.""" return self.coordinator.risco + + +class RiscoCloudZoneEntity(RiscoCloudEntity): + """Risco cloud zone entity base class.""" + + _attr_has_entity_name = True + + def __init__( + self, + *, + coordinator: RiscoDataUpdateCoordinator, + name: str | None, + suffix: str, + zone_id: int, + zone: Zone, + **kwargs: Any, + ) -> None: + """Init the zone.""" + super().__init__(coordinator=coordinator, **kwargs) + self._zone_id = zone_id + self._zone = zone + self._attr_name = name + device_unique_id = zone_unique_id(self._risco, zone_id) + self._attr_unique_id = f"{device_unique_id}{suffix}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_unique_id)}, + manufacturer="Risco", + name=self._zone.name, + ) + self._attr_extra_state_attributes = {"zone_id": zone_id} + + def _get_data_from_coordinator(self) -> None: + self._zone = self.coordinator.data.zones[self._zone_id] + + +class RiscoLocalZoneEntity(Entity): + """Risco local zone entity base class.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + *, + system_id: str, + name: str | None, + suffix: str, + zone_id: int, + zone: Zone, + **kwargs: Any, + ) -> None: + """Init the zone.""" + super().__init__(**kwargs) + self._zone_id = zone_id + self._zone = zone + self._attr_name = name + device_unique_id = f"{system_id}_zone_{zone_id}_local" + self._attr_unique_id = f"{device_unique_id}{suffix}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_unique_id)}, + manufacturer="Risco", + name=zone.name, + ) + self._attr_extra_state_attributes = {"zone_id": zone_id} + + async def async_added_to_hass(self) -> None: + """Subscribe to updates.""" + signal = zone_update_signal(self._zone_id) + self.async_on_remove( + async_dispatcher_connect(self.hass, signal, self.async_write_ha_state) + ) diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index c4bd047e260055..f2cb98211660b9 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -15,7 +15,7 @@ from . import RiscoEventsDataUpdateCoordinator, is_local from .const import DOMAIN, EVENTS_COORDINATOR -from .entity import binary_sensor_unique_id +from .entity import zone_unique_id CATEGORIES = { 2: "Alarm", @@ -115,11 +115,9 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: attrs = {atr: getattr(self._event, atr, None) for atr in EVENT_ATTRIBUTES} if self._event.zone_id is not None: - zone_unique_id = binary_sensor_unique_id( - self.coordinator.risco, self._event.zone_id - ) + uid = zone_unique_id(self.coordinator.risco, self._event.zone_id) zone_entity_id = self._entity_registry.async_get_entity_id( - BS_DOMAIN, DOMAIN, zone_unique_id + BS_DOMAIN, DOMAIN, uid ) if zone_entity_id is not None: attrs["zone_entity_id"] = zone_entity_id diff --git a/homeassistant/components/risco/services.yaml b/homeassistant/components/risco/services.yaml deleted file mode 100644 index c271df7b462acb..00000000000000 --- a/homeassistant/components/risco/services.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# Describes the format for available Risco services - -bypass_zone: - name: Bypass zone - description: Bypass a Risco Zone - target: - entity: - integration: risco - domain: binary_sensor - -unbypass_zone: - name: Unbypass zone - description: Unbypass a Risco Zone - target: - entity: - integration: risco - domain: binary_sensor diff --git a/homeassistant/components/risco/switch.py b/homeassistant/components/risco/switch.py new file mode 100644 index 00000000000000..2ed07b9f34b891 --- /dev/null +++ b/homeassistant/components/risco/switch.py @@ -0,0 +1,104 @@ +"""Support for bypassing Risco alarm zones.""" +from __future__ import annotations + +from pyrisco.common import Zone + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import LocalData, RiscoDataUpdateCoordinator, is_local +from .const import DATA_COORDINATOR, DOMAIN +from .entity import RiscoCloudZoneEntity, RiscoLocalZoneEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Risco switch.""" + if is_local(config_entry): + local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + RiscoLocalSwitch(local_data.system.id, zone_id, zone) + for zone_id, zone in local_data.system.zones.items() + ) + else: + coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ][DATA_COORDINATOR] + async_add_entities( + RiscoCloudSwitch(coordinator, zone_id, zone) + for zone_id, zone in coordinator.data.zones.items() + ) + + +class RiscoCloudSwitch(RiscoCloudZoneEntity, SwitchEntity): + """Representation of a bypass switch for a Risco cloud zone.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__( + self, coordinator: RiscoDataUpdateCoordinator, zone_id: int, zone: Zone + ) -> None: + """Init the zone.""" + super().__init__( + coordinator=coordinator, + name="Bypassed", + suffix="_bypassed", + zone_id=zone_id, + zone=zone, + ) + + @property + def is_on(self) -> bool | None: + """Return true if the zone is bypassed.""" + return self._zone.bypassed + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + await self._bypass(True) + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + await self._bypass(False) + + async def _bypass(self, bypass: bool) -> None: + alarm = await self._risco.bypass_zone(self._zone_id, bypass) + self._zone = alarm.zones[self._zone_id] + self.async_write_ha_state() + + +class RiscoLocalSwitch(RiscoLocalZoneEntity, SwitchEntity): + """Representation of a bypass switch for a Risco local zone.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: + """Init the zone.""" + super().__init__( + system_id=system_id, + name="Bypassed", + suffix="_bypassed", + zone_id=zone_id, + zone=zone, + ) + + @property + def is_on(self) -> bool | None: + """Return true if the zone is bypassed.""" + return self._zone.bypassed + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + await self._bypass(True) + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + await self._bypass(False) + + async def _bypass(self, bypass: bool) -> None: + await self._zone.bypass(bypass) diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index b22768a1cd0d3a..cc65efd9b555a3 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -39,10 +39,14 @@ def two_zone_cloud(): zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) ), patch.object( zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), patch.object( + zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) ), patch.object( zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) ), patch.object( zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), patch.object( + zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) ), patch.object( alarm_mock, "zones", @@ -54,6 +58,36 @@ def two_zone_cloud(): yield zone_mocks +@fixture +def two_zone_local(): + """Fixture to mock alarm with two zones.""" + zone_mocks = {0: zone_mock(), 1: zone_mock()} + with patch.object( + zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) + ), patch.object( + zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") + ), patch.object( + zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) + ), patch.object( + zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") + ), patch.object( + zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) + ), patch( + "homeassistant.components.risco.RiscoLocal.partitions", + new_callable=PropertyMock(return_value={}), + ), patch( + "homeassistant.components.risco.RiscoLocal.zones", + new_callable=PropertyMock(return_value=zone_mocks), + ): + yield zone_mocks + + @fixture def options(): """Fixture for default (empty) options.""" diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 71cbd04f391458..00d10f6059e43d 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -9,7 +9,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_component import async_update_entity -from .util import TEST_SITE_UUID, zone_mock +from .util import TEST_SITE_UUID FIRST_ENTITY_ID = "binary_sensor.zone_0" SECOND_ENTITY_ID = "binary_sensor.zone_1" @@ -17,32 +17,6 @@ SECOND_ALARMED_ENTITY_ID = SECOND_ENTITY_ID + "_alarmed" -@pytest.fixture -def two_zone_local(): - """Fixture to mock alarm with two zones.""" - zone_mocks = {0: zone_mock(), 1: zone_mock()} - with patch.object( - zone_mocks[0], "id", new_callable=PropertyMock(return_value=0) - ), patch.object( - zone_mocks[0], "name", new_callable=PropertyMock(return_value="Zone 0") - ), patch.object( - zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False) - ), patch.object( - zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) - ), patch.object( - zone_mocks[1], "name", new_callable=PropertyMock(return_value="Zone 1") - ), patch.object( - zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False) - ), patch( - "homeassistant.components.risco.RiscoLocal.partitions", - new_callable=PropertyMock(return_value={}), - ), patch( - "homeassistant.components.risco.RiscoLocal.zones", - new_callable=PropertyMock(return_value=zone_mocks), - ): - yield zone_mocks - - @pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) async def test_error_on_login(hass, login_with_error, cloud_config_entry): """Test error on login.""" @@ -69,59 +43,26 @@ async def test_cloud_setup(hass, two_zone_cloud, setup_risco_cloud): assert device.manufacturer == "Risco" -async def _check_cloud_state(hass, zones, triggered, bypassed, entity_id, zone_id): +async def _check_cloud_state(hass, zones, triggered, entity_id, zone_id): with patch.object( zones[zone_id], "triggered", new_callable=PropertyMock(return_value=triggered), - ), patch.object( - zones[zone_id], - "bypassed", - new_callable=PropertyMock(return_value=bypassed), ): await async_update_entity(hass, entity_id) await hass.async_block_till_done() expected_triggered = STATE_ON if triggered else STATE_OFF assert hass.states.get(entity_id).state == expected_triggered - assert hass.states.get(entity_id).attributes["bypassed"] == bypassed assert hass.states.get(entity_id).attributes["zone_id"] == zone_id async def test_cloud_states(hass, two_zone_cloud, setup_risco_cloud): """Test the various alarm states.""" - await _check_cloud_state(hass, two_zone_cloud, True, True, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, True, False, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, False, True, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, False, False, FIRST_ENTITY_ID, 0) - await _check_cloud_state(hass, two_zone_cloud, True, True, SECOND_ENTITY_ID, 1) - await _check_cloud_state(hass, two_zone_cloud, True, False, SECOND_ENTITY_ID, 1) - await _check_cloud_state(hass, two_zone_cloud, False, True, SECOND_ENTITY_ID, 1) - await _check_cloud_state(hass, two_zone_cloud, False, False, SECOND_ENTITY_ID, 1) - - -async def test_cloud_bypass(hass, two_zone_cloud, setup_risco_cloud): - """Test bypassing a zone.""" - with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: - data = {"entity_id": FIRST_ENTITY_ID} - - await hass.services.async_call( - DOMAIN, "bypass_zone", service_data=data, blocking=True - ) - - mock.assert_awaited_once_with(0, True) - - -async def test_cloud_unbypass(hass, two_zone_cloud, setup_risco_cloud): - """Test unbypassing a zone.""" - with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: - data = {"entity_id": FIRST_ENTITY_ID} - - await hass.services.async_call( - DOMAIN, "unbypass_zone", service_data=data, blocking=True - ) - - mock.assert_awaited_once_with(0, False) + await _check_cloud_state(hass, two_zone_cloud, True, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, False, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, True, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, False, SECOND_ENTITY_ID, 1) @pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) @@ -154,24 +95,17 @@ async def test_local_setup(hass, two_zone_local, setup_risco_local): assert device.manufacturer == "Risco" -async def _check_local_state( - hass, zones, triggered, bypassed, entity_id, zone_id, callback -): +async def _check_local_state(hass, zones, triggered, entity_id, zone_id, callback): with patch.object( zones[zone_id], "triggered", new_callable=PropertyMock(return_value=triggered), - ), patch.object( - zones[zone_id], - "bypassed", - new_callable=PropertyMock(return_value=bypassed), ): await callback(zone_id, zones[zone_id]) await hass.async_block_till_done() expected_triggered = STATE_ON if triggered else STATE_OFF assert hass.states.get(entity_id).state == expected_triggered - assert hass.states.get(entity_id).attributes["bypassed"] == bypassed assert hass.states.get(entity_id).attributes["zone_id"] == zone_id @@ -205,30 +139,10 @@ async def test_local_states( assert callback is not None - await _check_local_state( - hass, two_zone_local, True, True, FIRST_ENTITY_ID, 0, callback - ) - await _check_local_state( - hass, two_zone_local, True, False, FIRST_ENTITY_ID, 0, callback - ) - await _check_local_state( - hass, two_zone_local, False, True, FIRST_ENTITY_ID, 0, callback - ) - await _check_local_state( - hass, two_zone_local, False, False, FIRST_ENTITY_ID, 0, callback - ) - await _check_local_state( - hass, two_zone_local, True, True, SECOND_ENTITY_ID, 1, callback - ) - await _check_local_state( - hass, two_zone_local, True, False, SECOND_ENTITY_ID, 1, callback - ) - await _check_local_state( - hass, two_zone_local, False, True, SECOND_ENTITY_ID, 1, callback - ) - await _check_local_state( - hass, two_zone_local, False, False, SECOND_ENTITY_ID, 1, callback - ) + await _check_local_state(hass, two_zone_local, True, FIRST_ENTITY_ID, 0, callback) + await _check_local_state(hass, two_zone_local, False, FIRST_ENTITY_ID, 0, callback) + await _check_local_state(hass, two_zone_local, True, SECOND_ENTITY_ID, 1, callback) + await _check_local_state(hass, two_zone_local, False, SECOND_ENTITY_ID, 1, callback) async def test_alarmed_local_states( @@ -251,27 +165,3 @@ async def test_alarmed_local_states( await _check_alarmed_local_state( hass, two_zone_local, False, SECOND_ALARMED_ENTITY_ID, 1, callback ) - - -async def test_local_bypass(hass, two_zone_local, setup_risco_local): - """Test bypassing a zone.""" - with patch.object(two_zone_local[0], "bypass") as mock: - data = {"entity_id": FIRST_ENTITY_ID} - - await hass.services.async_call( - DOMAIN, "bypass_zone", service_data=data, blocking=True - ) - - mock.assert_awaited_once_with(True) - - -async def test_local_unbypass(hass, two_zone_local, setup_risco_local): - """Test unbypassing a zone.""" - with patch.object(two_zone_local[0], "bypass") as mock: - data = {"entity_id": FIRST_ENTITY_ID} - - await hass.services.async_call( - DOMAIN, "unbypass_zone", service_data=data, blocking=True - ) - - mock.assert_awaited_once_with(False) diff --git a/tests/components/risco/test_switch.py b/tests/components/risco/test_switch.py new file mode 100644 index 00000000000000..5ea4e72abcac42 --- /dev/null +++ b/tests/components/risco/test_switch.py @@ -0,0 +1,151 @@ +"""Tests for the Risco binary sensors.""" +from unittest.mock import PropertyMock, patch + +import pytest + +from homeassistant.components.risco import CannotConnectError, UnauthorizedError +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity + +FIRST_ENTITY_ID = "switch.zone_0_bypassed" +SECOND_ENTITY_ID = "switch.zone_1_bypassed" + + +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_login(hass, login_with_error, cloud_config_entry): + """Test error on login.""" + await hass.config_entries.async_setup(cloud_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_cloud_setup(hass, two_zone_cloud, setup_risco_cloud): + """Test entity setup.""" + registry = er.async_get(hass) + assert registry.async_is_registered(FIRST_ENTITY_ID) + assert registry.async_is_registered(SECOND_ENTITY_ID) + + +async def _check_cloud_state(hass, zones, bypassed, entity_id, zone_id): + with patch.object( + zones[zone_id], + "bypassed", + new_callable=PropertyMock(return_value=bypassed), + ): + await async_update_entity(hass, entity_id) + await hass.async_block_till_done() + + expected_bypassed = STATE_ON if bypassed else STATE_OFF + assert hass.states.get(entity_id).state == expected_bypassed + assert hass.states.get(entity_id).attributes["zone_id"] == zone_id + + +async def test_cloud_states(hass, two_zone_cloud, setup_risco_cloud): + """Test the various alarm states.""" + await _check_cloud_state(hass, two_zone_cloud, True, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, False, FIRST_ENTITY_ID, 0) + await _check_cloud_state(hass, two_zone_cloud, True, SECOND_ENTITY_ID, 1) + await _check_cloud_state(hass, two_zone_cloud, False, SECOND_ENTITY_ID, 1) + + +async def test_cloud_bypass(hass, two_zone_cloud, setup_risco_cloud): + """Test bypassing a zone.""" + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(0, True) + + +async def test_cloud_unbypass(hass, two_zone_cloud, setup_risco_cloud): + """Test unbypassing a zone.""" + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(0, False) + + +@pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) +async def test_error_on_connect(hass, connect_with_error, local_config_entry): + """Test error on connect.""" + await hass.config_entries.async_setup(local_config_entry.entry_id) + await hass.async_block_till_done() + registry = er.async_get(hass) + assert not registry.async_is_registered(FIRST_ENTITY_ID) + assert not registry.async_is_registered(SECOND_ENTITY_ID) + + +async def test_local_setup(hass, two_zone_local, setup_risco_local): + """Test entity setup.""" + registry = er.async_get(hass) + assert registry.async_is_registered(FIRST_ENTITY_ID) + assert registry.async_is_registered(SECOND_ENTITY_ID) + + +async def _check_local_state(hass, zones, bypassed, entity_id, zone_id, callback): + with patch.object( + zones[zone_id], + "bypassed", + new_callable=PropertyMock(return_value=bypassed), + ): + await callback(zone_id, zones[zone_id]) + await hass.async_block_till_done() + + expected_bypassed = STATE_ON if bypassed else STATE_OFF + assert hass.states.get(entity_id).state == expected_bypassed + assert hass.states.get(entity_id).attributes["zone_id"] == zone_id + + +@pytest.fixture +def _mock_zone_handler(): + with patch("homeassistant.components.risco.RiscoLocal.add_zone_handler") as mock: + yield mock + + +async def test_local_states( + hass, two_zone_local, _mock_zone_handler, setup_risco_local +): + """Test the various alarm states.""" + callback = _mock_zone_handler.call_args.args[0] + + assert callback is not None + + await _check_local_state(hass, two_zone_local, True, FIRST_ENTITY_ID, 0, callback) + await _check_local_state(hass, two_zone_local, False, FIRST_ENTITY_ID, 0, callback) + await _check_local_state(hass, two_zone_local, True, SECOND_ENTITY_ID, 1, callback) + await _check_local_state(hass, two_zone_local, False, SECOND_ENTITY_ID, 1, callback) + + +async def test_local_bypass(hass, two_zone_local, setup_risco_local): + """Test bypassing a zone.""" + with patch.object(two_zone_local[0], "bypass") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(True) + + +async def test_local_unbypass(hass, two_zone_local, setup_risco_local): + """Test unbypassing a zone.""" + with patch.object(two_zone_local[0], "bypass") as mock: + data = {"entity_id": FIRST_ENTITY_ID} + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, service_data=data, blocking=True + ) + + mock.assert_awaited_once_with(False) From 0bca9a614cdc5b76e29e279c9a4b5f4612d3752c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 1 Nov 2022 00:33:48 +0000 Subject: [PATCH 109/394] [ci skip] Translation update --- .../components/aranet/translations/bg.json | 24 +++++++++++++++++ .../components/aranet/translations/ca.json | 22 ++++++++++++++++ .../components/aranet/translations/de.json | 25 ++++++++++++++++++ .../components/aranet/translations/en.json | 2 +- .../components/aranet/translations/es.json | 25 ++++++++++++++++++ .../components/aranet/translations/et.json | 25 ++++++++++++++++++ .../components/aranet/translations/hu.json | 25 ++++++++++++++++++ .../components/aranet/translations/no.json | 25 ++++++++++++++++++ .../components/aranet/translations/pt-BR.json | 25 ++++++++++++++++++ .../components/aranet/translations/ru.json | 25 ++++++++++++++++++ .../fireservicerota/translations/ca.json | 6 +++++ .../fireservicerota/translations/en.json | 6 +++++ .../fireservicerota/translations/es.json | 6 +++++ .../fireservicerota/translations/hu.json | 6 +++++ .../fireservicerota/translations/pt-BR.json | 6 +++++ .../google_travel_time/translations/no.json | 1 + .../translations/pt-BR.json | 1 + .../components/hassio/translations/ca.json | 10 +++++++ .../components/hassio/translations/en.json | 10 +++++++ .../components/hassio/translations/es.json | 10 +++++++ .../components/hassio/translations/et.json | 10 +++++++ .../components/hassio/translations/hu.json | 10 +++++++ .../components/hassio/translations/pt-BR.json | 10 +++++++ .../components/hassio/translations/ru.json | 10 +++++++ .../components/mqtt/translations/bg.json | 13 ++++++++-- .../ovo_energy/translations/no.json | 1 + .../ovo_energy/translations/pt-BR.json | 1 + .../rainmachine/translations/no.json | 1 + .../components/scrape/translations/bg.json | 5 ++++ .../components/scrape/translations/et.json | 6 +++++ .../components/scrape/translations/hu.json | 6 +++++ .../components/scrape/translations/no.json | 6 +++++ .../components/scrape/translations/pt-BR.json | 6 +++++ .../statistics/translations/no.json | 12 +++++++++ .../transmission/translations/hu.json | 13 ++++++++++ .../transmission/translations/no.json | 13 ++++++++++ .../transmission/translations/pt-BR.json | 13 ++++++++++ .../components/zamg/translations/no.json | 26 +++++++++++++++++++ .../components/zha/translations/es.json | 2 +- 39 files changed, 445 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/aranet/translations/bg.json create mode 100644 homeassistant/components/aranet/translations/ca.json create mode 100644 homeassistant/components/aranet/translations/de.json create mode 100644 homeassistant/components/aranet/translations/es.json create mode 100644 homeassistant/components/aranet/translations/et.json create mode 100644 homeassistant/components/aranet/translations/hu.json create mode 100644 homeassistant/components/aranet/translations/no.json create mode 100644 homeassistant/components/aranet/translations/pt-BR.json create mode 100644 homeassistant/components/aranet/translations/ru.json create mode 100644 homeassistant/components/statistics/translations/no.json create mode 100644 homeassistant/components/zamg/translations/no.json diff --git a/homeassistant/components/aranet/translations/bg.json b/homeassistant/components/aranet/translations/bg.json new file mode 100644 index 00000000000000..19937c000c862e --- /dev/null +++ b/homeassistant/components/aranet/translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 Aranet \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "outdated_version": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0441\u0442\u0430\u0440\u044f\u043b \u0444\u044a\u0440\u043c\u0443\u0435\u0440. \u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0433\u043e \u043f\u043e\u043d\u0435 \u0434\u043e v1.2.0 \u0438 \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/ca.json b/homeassistant/components/aranet/translations/ca.json new file mode 100644 index 00000000000000..1bbc15cc1b71a9 --- /dev/null +++ b/homeassistant/components/aranet/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/de.json b/homeassistant/components/aranet/translations/de.json new file mode 100644 index 00000000000000..cc8a35e3761b4e --- /dev/null +++ b/homeassistant/components/aranet/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "integrations_diabled": "Auf diesem Ger\u00e4t sind keine Integrationen aktiviert. Bitte aktiviere die Smart-Home-Integration \u00fcber die App und versuche es erneut.", + "no_devices_found": "Keine unkonfigurierten Aranet-Ger\u00e4te gefunden.", + "outdated_version": "Dieses Ger\u00e4t verwendet eine veraltete Firmware. Bitte aktualisiere es auf mindestens v1.2.0 und versuche es erneut." + }, + "error": { + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/en.json b/homeassistant/components/aranet/translations/en.json index 303fd56f1c8938..00d5aacf11c06d 100644 --- a/homeassistant/components/aranet/translations/en.json +++ b/homeassistant/components/aranet/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Device is already configured", "integrations_diabled": "This device doesn't have integrations enabled. Please enable smart home integrations using the app and try again.", - "no_devices_found": "No unconfigured Aranet4 devices found.", + "no_devices_found": "No unconfigured Aranet devices found.", "outdated_version": "This device is using outdated firmware. Please update it to at least v1.2.0 and try again." }, "error": { diff --git a/homeassistant/components/aranet/translations/es.json b/homeassistant/components/aranet/translations/es.json new file mode 100644 index 00000000000000..c2e96ecfc7a5f3 --- /dev/null +++ b/homeassistant/components/aranet/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "integrations_diabled": "Este dispositivo no tiene integraciones habilitadas. Por favor, habilita las integraciones de hogares inteligentes usando la aplicaci\u00f3n y int\u00e9ntalo de nuevo.", + "no_devices_found": "No se han encontrado dispositivos Aranet no configurados.", + "outdated_version": "Este dispositivo est\u00e1 utilizando firmware obsoleto. Por favor, actual\u00edzalo al menos a v1.2.0 e int\u00e9ntalo de nuevo." + }, + "error": { + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/et.json b/homeassistant/components/aranet/translations/et.json new file mode 100644 index 00000000000000..989add44454e04 --- /dev/null +++ b/homeassistant/components/aranet/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "integrations_diabled": "Sellel seadmel pole sidumised lubatud. Luba rakenduse abil nutika kodu sidumine ja proovi uuesti.", + "no_devices_found": "H\u00e4\u00e4lestamata Araneti seadmeid ei leitud.", + "outdated_version": "See seade kasutab aegunud p\u00fcsivara. V\u00e4rskenda see v\u00e4hemalt versioonile 1.2.0 ja proovi uuesti." + }, + "error": { + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/hu.json b/homeassistant/components/aranet/translations/hu.json new file mode 100644 index 00000000000000..2773a11e3dc508 --- /dev/null +++ b/homeassistant/components/aranet/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "integrations_diabled": "Ezen az eszk\u00f6z\u00f6n nincs enged\u00e9lyezve az integr\u00e1ci\u00f3. K\u00e9rj\u00fck, enged\u00e9lyezze az okosotthon-integr\u00e1ci\u00f3kat az alkalmaz\u00e1s seg\u00edts\u00e9g\u00e9vel, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan Aranet eszk\u00f6z.", + "outdated_version": "Ez az eszk\u00f6z elavult firmware-t haszn\u00e1l. K\u00e9rj\u00fck, friss\u00edtse legal\u00e1bb 1.2.0-s verzi\u00f3ra, \u00e9s pr\u00f3b\u00e1lja \u00fajra." + }, + "error": { + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/no.json b/homeassistant/components/aranet/translations/no.json new file mode 100644 index 00000000000000..8e4a732972a9c1 --- /dev/null +++ b/homeassistant/components/aranet/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "integrations_diabled": "Denne enheten har ikke integrasjoner aktivert. Aktiver smarthus-integrasjoner ved hjelp av appen og pr\u00f8v igjen.", + "no_devices_found": "Fant ingen ukonfigurerte Aranet-enheter.", + "outdated_version": "Denne enheten bruker utdatert fastvare. Oppdater den til minst v1.2.0 og pr\u00f8v igjen." + }, + "error": { + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/pt-BR.json b/homeassistant/components/aranet/translations/pt-BR.json new file mode 100644 index 00000000000000..f98a79dabaa823 --- /dev/null +++ b/homeassistant/components/aranet/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "integrations_diabled": "Este dispositivo n\u00e3o tem integra\u00e7\u00f5es ativadas. Ative as integra\u00e7\u00f5es de casa inteligente usando o aplicativo e tente novamente.", + "no_devices_found": "Nenhum dispositivo Aranet n\u00e3o configurado encontrado.", + "outdated_version": "Este dispositivo est\u00e1 usando um firmware desatualizado. Atualize-o para pelo menos a v1.2.0 e tente novamente." + }, + "error": { + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/ru.json b/homeassistant/components/aranet/translations/ru.json new file mode 100644 index 00000000000000..11477454e63b92 --- /dev/null +++ b/homeassistant/components/aranet/translations/ru.json @@ -0,0 +1,25 @@ +{ + "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.", + "integrations_diabled": "\u041d\u0430 \u044d\u0442\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f. \u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0443\u043c\u043d\u043e\u0433\u043e \u0434\u043e\u043c\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "no_devices_found": "\u041d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e.", + "outdated_version": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0443\u044e \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0435\u0451 \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u0434\u043e \u0432\u0435\u0440\u0441\u0438\u0438 1.2.0 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." + }, + "error": { + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/ca.json b/homeassistant/components/fireservicerota/translations/ca.json index 261350db3f8f91..9961f00f38fc7b 100644 --- a/homeassistant/components/fireservicerota/translations/ca.json +++ b/homeassistant/components/fireservicerota/translations/ca.json @@ -17,6 +17,12 @@ }, "description": "Els tokens d'autenticaci\u00f3 ja no s\u00f3n v\u00e0lids, inicia sessi\u00f3 per tornar-los a generar." }, + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Els 'tokens' d'autenticaci\u00f3 ja no s\u00f3n v\u00e0lids, inicia sessi\u00f3 per tornar-los a generar." + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/fireservicerota/translations/en.json b/homeassistant/components/fireservicerota/translations/en.json index 38762b614f4268..71c4f757b53f12 100644 --- a/homeassistant/components/fireservicerota/translations/en.json +++ b/homeassistant/components/fireservicerota/translations/en.json @@ -11,6 +11,12 @@ "invalid_auth": "Invalid authentication" }, "step": { + "reauth": { + "data": { + "password": "Password" + }, + "description": "Authentication tokens became invalid, login to recreate them." + }, "reauth_confirm": { "data": { "password": "Password" diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index ddd231ce700f3b..297653c7708d59 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -17,6 +17,12 @@ }, "description": "Los tokens de autenticaci\u00f3n dejaron de ser v\u00e1lidos, inicia sesi\u00f3n para volver a crearlos." }, + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "Los tokens de autenticaci\u00f3n dejaron de ser v\u00e1lidos, inicia sesi\u00f3n para volver a crearlos." + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json index 3bda2225400e6e..47ce01be5d3062 100644 --- a/homeassistant/components/fireservicerota/translations/hu.json +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -17,6 +17,12 @@ }, "description": "A hiteles\u00edt\u00e9si tokenek \u00e9rv\u00e9nytelenn\u00e9 v\u00e1ltak, a l\u00e9trehoz\u00e1shoz jelentkezzen be." }, + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A hiteles\u00edt\u00e9si tokenek \u00e9rv\u00e9nytelenn\u00e9 v\u00e1ltak, a l\u00e9trehoz\u00e1shoz jelentkezzen be." + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/fireservicerota/translations/pt-BR.json b/homeassistant/components/fireservicerota/translations/pt-BR.json index 45b55aa5324471..9e6abd2964a5c1 100644 --- a/homeassistant/components/fireservicerota/translations/pt-BR.json +++ b/homeassistant/components/fireservicerota/translations/pt-BR.json @@ -17,6 +17,12 @@ }, "description": "Os tokens de autentica\u00e7\u00e3o se tornaram inv\u00e1lidos, fa\u00e7a login para recri\u00e1-los." }, + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Os tokens de autentica\u00e7\u00e3o se tornaram inv\u00e1lidos, fa\u00e7a login para recri\u00e1-los." + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/google_travel_time/translations/no.json b/homeassistant/components/google_travel_time/translations/no.json index 2a056451bbe81f..0a9cb3e6d8361d 100644 --- a/homeassistant/components/google_travel_time/translations/no.json +++ b/homeassistant/components/google_travel_time/translations/no.json @@ -28,6 +28,7 @@ "mode": "Reisemodus", "time": "Tid", "time_type": "Tidstype", + "traffic_mode": "Trafikkmodus", "transit_mode": "Transittmodus", "transit_routing_preference": "Ruteinnstillinger for kollektivtransport", "units": "Enheter" diff --git a/homeassistant/components/google_travel_time/translations/pt-BR.json b/homeassistant/components/google_travel_time/translations/pt-BR.json index 9b8249b1b518fb..baa16f38f35f9e 100644 --- a/homeassistant/components/google_travel_time/translations/pt-BR.json +++ b/homeassistant/components/google_travel_time/translations/pt-BR.json @@ -28,6 +28,7 @@ "mode": "Modo de viagem", "time": "Tempo", "time_type": "Tipo de tempo", + "traffic_mode": "Modo de tr\u00e1fego", "transit_mode": "Modo de tr\u00e2nsito", "transit_routing_preference": "Prefer\u00eancia de rota de tr\u00e2nsito", "units": "Unidades" diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 2c4285d49084f4..14679301993a1f 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.", + "title": "Sistema no saludable - {reason}" + }, + "unsupported": { + "description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.", + "title": "Sistema no compatible - {reason}" + } + }, "system_health": { "info": { "agent_version": "Versi\u00f3 de l'agent", diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 14d79f0d8d6c8c..b6f006e30932c5 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it.", + "title": "Unhealthy system - {reason}" + }, + "unsupported": { + "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system.", + "title": "Unsupported system - {reason}" + } + }, "system_health": { "info": { "agent_version": "Agent Version", diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 102256ef1173f3..f2aef9d7214b73 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "Actualmente el sistema no est\u00e1 en buen estado debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que est\u00e1 mal y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado: {reason}" + }, + "unsupported": { + "description": "El sistema no es compatible debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que esto significa y c\u00f3mo volver a un sistema compatible.", + "title": "Sistema no compatible: {reason}" + } + }, "system_health": { "info": { "agent_version": "Versi\u00f3n del agente", diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index b86eef353b962b..ea0f78c0c57c0d 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "S\u00fcsteem ei ole praegu korras '{reason}' t\u00f5ttu. Kasuta linki, et saada rohkem teavet selle kohta, mis on valesti ja kuidas seda parandada.", + "title": "Vigane s\u00fcsteem \u2013 {reason}" + }, + "unsupported": { + "description": "S\u00fcsteemi ei toetata '{reason}' t\u00f5ttu. Kasuta linki, et saada lisateavet selle kohta, mida see t\u00e4hendab ja kuidas toetatud s\u00fcsteemi naasta.", + "title": "Toetamata s\u00fcsteem \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "Agendi versioon", diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 4c83b94935d43b..604a8ae59e63f1 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "A rendszer jelenleg renellenes \u00e1llapotban van '{reason}' miatt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet is megtudhat arr\u00f3l, hogy mi a probl\u00e9ma, \u00e9s hogyan jav\u00edthatja ki.", + "title": "Rendellenes \u00e1llapot \u2013 {reason}" + }, + "unsupported": { + "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: '{reason}'. A hivatkoz\u00e1s seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat arr\u00f3l, mit jelent ez, \u00e9s hogyan t\u00e9rhet vissza egy t\u00e1mogatott rendszerhez.", + "title": "Nem t\u00e1mogatott rendszer \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "\u00dcgyn\u00f6k verzi\u00f3", diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json index 4f3e5d84ec1699..47e0b6df4aed6b 100644 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a '{reason}'. Use o link para saber mais sobre o que est\u00e1 errado e como corrigi-lo.", + "title": "Sistema insalubre - {reason}" + }, + "unsupported": { + "description": "O sistema n\u00e3o \u00e9 suportado devido a '{reason}'. Use o link para saber mais sobre o que isso significa e como retornar a um sistema compat\u00edvel.", + "title": "Sistema n\u00e3o suportado - {reason}" + } + }, "system_health": { "info": { "agent_version": "Vers\u00e3o do Agent", diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 5e1caa41ebf56c..0ab366c1775ef0 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + }, + "unsupported": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0438 \u043a\u0430\u043a \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + } + }, "system_health": { "info": { "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430", diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index f99f120d9516ef..06751555b295b1 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -6,17 +6,20 @@ }, "error": { "bad_certificate": "CA \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0431\u0440\u043e\u043a\u0435\u0440\u0430." + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0431\u0440\u043e\u043a\u0435\u0440\u0430.", + "invalid_inclusion": "\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u044f\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0438 \u0447\u0430\u0441\u0442\u043d\u0438\u044f\u0442 \u043a\u043b\u044e\u0447 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0437\u0430\u0435\u0434\u043d\u043e" }, "step": { "broker": { "data": { "advanced_options": "\u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438", "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", + "client_id": "ID \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d)", "discovery": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0442\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "protocol": "MQTT \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "set_client_cert": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f MQTT \u0431\u0440\u043e\u043a\u0435\u0440." @@ -40,15 +43,21 @@ "options": { "error": { "bad_certificate": "CA \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_inclusion": "\u041a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438\u044f\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0438 \u0447\u0430\u0441\u0442\u043d\u0438\u044f\u0442 \u043a\u043b\u044e\u0447 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0437\u0430\u0435\u0434\u043d\u043e" }, "step": { "broker": { "data": { "advanced_options": "\u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043d\u0438 \u043e\u043f\u0446\u0438\u0438", + "certificate": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b \u0441 \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043d\u0430 CA", + "client_cert": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b \u0441 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", + "client_id": "ID \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d)", + "client_key": "\u041a\u0430\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 \u0444\u0430\u0439\u043b \u0441 \u0447\u0430\u0441\u0442\u0435\u043d \u043a\u043b\u044e\u0447", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "protocol": "MQTT \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "set_client_cert": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 64b989de78b7f9..23473cca581332 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO-konto-ID (bare legg til hvis du har flere kontoer)", "password": "Passord", "username": "Brukernavn" }, diff --git a/homeassistant/components/ovo_energy/translations/pt-BR.json b/homeassistant/components/ovo_energy/translations/pt-BR.json index 4b73ecca8b9438..7af4adf23898a0 100644 --- a/homeassistant/components/ovo_energy/translations/pt-BR.json +++ b/homeassistant/components/ovo_energy/translations/pt-BR.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID da conta OVO (adicione apenas se voc\u00ea tiver v\u00e1rias contas)", "password": "Senha", "username": "Usu\u00e1rio" }, diff --git a/homeassistant/components/rainmachine/translations/no.json b/homeassistant/components/rainmachine/translations/no.json index f7e0a758ee05e8..f738be0ad7dd0d 100644 --- a/homeassistant/components/rainmachine/translations/no.json +++ b/homeassistant/components/rainmachine/translations/no.json @@ -35,6 +35,7 @@ "step": { "init": { "data": { + "use_app_run_times": "Bruk sonekj\u00f8ringstider fra RainMachine-appen", "zone_run_time": "Standard sonekj\u00f8ringstid (i sekunder)" }, "title": "Konfigurer RainMachine" diff --git a/homeassistant/components/scrape/translations/bg.json b/homeassistant/components/scrape/translations/bg.json index 1599a1918d766b..095249f5317bda 100644 --- a/homeassistant/components/scrape/translations/bg.json +++ b/homeassistant/components/scrape/translations/bg.json @@ -18,6 +18,11 @@ } } }, + "issues": { + "moved_yaml": { + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Scrape \u0435 \u043f\u0440\u0435\u043c\u0435\u0441\u0442\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/et.json b/homeassistant/components/scrape/translations/et.json index 14daf835af8a83..7168041b566a68 100644 --- a/homeassistant/components/scrape/translations/et.json +++ b/homeassistant/components/scrape/translations/et.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "Scrape'i konfigureerimine YAML-i abil on viidud integratsiooniv\u00f5tmesse.\n\nOlemasolev YAML-konfiguratsioon t\u00f6\u00f6tab veel 2 versiooni.\n\nMigreeri YAML-konfiguratsioon integratsiooniv\u00f5tmesse vastavalt dokumentatsioonile.", + "title": "Scrape YAML-i konfiguratsioon on teisaldatud" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/hu.json b/homeassistant/components/scrape/translations/hu.json index 7af59751b98467..002891fb47dfb5 100644 --- a/homeassistant/components/scrape/translations/hu.json +++ b/homeassistant/components/scrape/translations/hu.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "A Scrape konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val \u00e1tker\u00fclt az integr\u00e1ci\u00f3ba.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 m\u00e9g 2 verzi\u00f3n kereszt\u00fcl fog m\u0171k\u00f6dni.\n\nA dokument\u00e1ci\u00f3nak megfelel\u0151en migr\u00e1lja a YAML konfigur\u00e1ci\u00f3j\u00e1t az integr\u00e1ci\u00f3s kulcsra.", + "title": "A Scrape YAML konfigur\u00e1ci\u00f3 \u00e1thelyez\u00e9sre ker\u00fclt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/no.json b/homeassistant/components/scrape/translations/no.json index 6738c8a630acb9..abf5580074a632 100644 --- a/homeassistant/components/scrape/translations/no.json +++ b/homeassistant/components/scrape/translations/no.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "Konfigurering av Scrape ved hjelp av YAML har blitt flyttet til integrasjonsn\u00f8kkel. \n\n Din eksisterende YAML-konfigurasjon vil fungere for 2 flere versjoner. \n\n Migrer YAML-konfigurasjonen til integrasjonsn\u00f8kkelen i henhold til dokumentasjonen.", + "title": "Scrape YAML-konfigurasjonen er flyttet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/pt-BR.json b/homeassistant/components/scrape/translations/pt-BR.json index 84d7aaf6807467..55e322ab6c9536 100644 --- a/homeassistant/components/scrape/translations/pt-BR.json +++ b/homeassistant/components/scrape/translations/pt-BR.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "A configura\u00e7\u00e3o do Scrape usando YAML foi movida para a chave de integra\u00e7\u00e3o. \n\n Sua configura\u00e7\u00e3o YAML existente funcionar\u00e1 para mais duas vers\u00f5es. \n\n Migre sua configura\u00e7\u00e3o YAML para a chave de integra\u00e7\u00e3o de acordo com a documenta\u00e7\u00e3o.", + "title": "A configura\u00e7\u00e3o YAML de Scrape foi movida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/statistics/translations/no.json b/homeassistant/components/statistics/translations/no.json new file mode 100644 index 00000000000000..c3e3369a59a083 --- /dev/null +++ b/homeassistant/components/statistics/translations/no.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "Konfigurasjonsparameteren `state_characteristic` for statistikkintegrasjonen vil bli obligatorisk. \n\n Vennligst legg til `state_characteristic: {characteristic} ` til konfigurasjonen av sensor ` {entity} ` for \u00e5 beholde gjeldende oppf\u00f8rsel. \n\n Les dokumentasjonen av statistikkintegrasjonen for ytterligere detaljer: https://www.home-assistant.io/integrations/statistics/", + "title": "Obligatorisk \"state_characteristic\" antatt for en statistikkenhet" + }, + "deprecation_warning_size": { + "description": "Konfigurasjonsparameteren `sampling_size` for statistikkintegrasjonen har standardverdien 20 s\u00e5 langt, som vil endres. \n\n Vennligst sjekk konfigurasjonen for sensor ` {entity} ` og legg til passende grenser, f.eks. `sampling_size: 20` for \u00e5 beholde gjeldende virkem\u00e5te. Konfigurasjonen av statistikkintegrasjonen vil bli mer fleksibel med versjon 2022.12.0 og godta enten `sampling_size` eller `max_age`, eller begge innstillingene. Foresp\u00f8rselen ovenfor forbereder konfigurasjonen din for denne ellers \u00f8deleggende endringen. \n\n Les dokumentasjonen av statistikkintegrasjonen for ytterligere detaljer: https://www.home-assistant.io/integrations/statistics/", + "title": "Implisitt 'sampling_size' antatt for en statistikkenhet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 1bd7129ed6b5b0..b9eae6b7f6ff93 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, \u00e9s cser\u00e9lje ki a name kulcsot a entry_id kulcsra.", + "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index dfe188f6e3b8e2..2a03ee48b74503 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten og erstatt navnen\u00f8kkelen med entry_id-n\u00f8kkelen.", + "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" + } + } + }, + "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index 5579b64e2d992f..878e911564d9d2 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o e substitua a chave de nome pela chave entry_id.", + "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" + } + } + }, + "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/zamg/translations/no.json b/homeassistant/components/zamg/translations/no.json new file mode 100644 index 00000000000000..265fff5c70d190 --- /dev/null +++ b/homeassistant/components/zamg/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "Stasjons-ID (standard til n\u00e6rmeste stasjon)" + }, + "description": "Konfigurer ZAMG for \u00e5 integrere med Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av ZAMG med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern ZAMG YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "ZAMG YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 2919302a1ac5a2..b31f1c10d78850 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -150,7 +150,7 @@ "device_flipped": "Dispositivo volteado \"{subtype}\"", "device_knocked": "Dispositivo golpeado \"{subtype}\"", "device_offline": "Dispositivo sin conexi\u00f3n", - "device_rotated": "Dispositivo girado \"{subtype}\"", + "device_rotated": "Dispositivo rotado \"{subtype}\"", "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \"{subtype}\"", "device_tilted": "Dispositivo inclinado", From d87ca0b09992e444b192074fef38e73e9341efe3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 20:21:11 -0500 Subject: [PATCH 110/394] Improve esphome bluetooth error reporting (#81326) --- homeassistant/components/esphome/bluetooth/client.py | 10 ++++++++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 5f20a73f4d6316..72531a2503a88c 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,7 +7,11 @@ from typing import Any, TypeVar, cast import uuid -from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError +from aioesphomeapi import ( + ESP_CONNECTION_ERROR_DESCRIPTION, + ESPHOME_GATT_ERRORS, + BLEConnectionError, +) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic @@ -207,7 +211,9 @@ def _on_bluetooth_connection_state( human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] except (KeyError, ValueError): ble_connection_error_name = str(error) - human_error = f"Unknown error code {error}" + human_error = ESPHOME_GATT_ERRORS.get( + error, f"Unknown error code {error}" + ) connected_future.set_exception( BleakError( f"Error {ble_connection_error_name} while connecting: {human_error}" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c27e3b8dc3e114..64cd6b4029c6aa 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==11.4.1"], + "requirements": ["aioesphomeapi==11.4.2"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 5fb9331bd2928e..be619a1239cdfd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.1 +aioesphomeapi==11.4.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca71daa39be8f9..73be12c5fea4b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.1 +aioesphomeapi==11.4.2 # homeassistant.components.flo aioflo==2021.11.0 From eea47195444b53f100d83a2a67ecba149b0acc1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 20:21:40 -0500 Subject: [PATCH 111/394] Fix Yale Access Bluetooth not being available again after being unavailable (#81320) --- homeassistant/components/yalexs_ble/__init__.py | 13 +++++++++++++ homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 6073bf7a0322c5..7a2b3146265f4d 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -94,6 +94,19 @@ def _async_update_ble( entry.title, push_lock ) + @callback + def _async_device_unavailable( + _service_info: bluetooth.BluetoothServiceInfoBleak, + ) -> None: + """Handle device not longer being seen by the bluetooth stack.""" + push_lock.reset_advertisement_state() + + entry.async_on_unload( + bluetooth.async_track_unavailable( + hass, _async_device_unavailable, push_lock.address + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 7bc8bde5b30aa0..b43ce18a7e9d76 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.9.4"], + "requirements": ["yalexs-ble==1.9.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index be619a1239cdfd..5b5b94d966b2ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2580,7 +2580,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.4 +yalexs-ble==1.9.5 # homeassistant.components.august yalexs==1.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73be12c5fea4b5..4a949ae7d13ec7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1790,7 +1790,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.4 +yalexs-ble==1.9.5 # homeassistant.components.august yalexs==1.2.6 From 52fe40d53913ab484578252014fb2a2442cf7f6e Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 1 Nov 2022 02:22:21 +0100 Subject: [PATCH 112/394] Only try initializing Hue motion LED on endpoint 2 with ZHA (#81205) --- homeassistant/components/zha/core/channels/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index ded51455af817b..c028a6021da9c5 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -156,7 +156,7 @@ class BasicChannel(ZigbeeChannel): def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize Basic channel.""" super().__init__(cluster, ch_pool) - if is_hue_motion_sensor(self): + if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2: self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name self.ZCL_INIT_ATTRS.copy() ) From 3ddcc637da42bd5c19c41441261fdfffe4ee794e Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Mon, 31 Oct 2022 09:57:54 -0400 Subject: [PATCH 113/394] Create repairs for unsupported and unhealthy (#80747) --- homeassistant/components/hassio/__init__.py | 6 + homeassistant/components/hassio/const.py | 21 +- homeassistant/components/hassio/handler.py | 8 + homeassistant/components/hassio/repairs.py | 138 ++++++ homeassistant/components/hassio/strings.json | 10 + tests/components/hassio/test_binary_sensor.py | 13 + tests/components/hassio/test_diagnostics.py | 13 + tests/components/hassio/test_init.py | 43 +- tests/components/hassio/test_repairs.py | 395 ++++++++++++++++++ tests/components/hassio/test_sensor.py | 13 + tests/components/hassio/test_update.py | 13 + tests/components/hassio/test_websocket_api.py | 13 + tests/components/http/test_ban.py | 12 +- tests/components/onboarding/test_views.py | 13 + 14 files changed, 690 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/hassio/repairs.py create mode 100644 tests/components/hassio/test_repairs.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 8535a0c3cc6eb1..c811b35812e7ea 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -77,6 +77,7 @@ from .handler import HassIO, HassioAPIError, api_data from .http import HassIOView from .ingress import async_setup_ingress_view +from .repairs import SupervisorRepairs from .websocket_api import async_load_websocket_api _LOGGER = logging.getLogger(__name__) @@ -103,6 +104,7 @@ DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs" DATA_ADDONS_INFO = "hassio_addons_info" DATA_ADDONS_STATS = "hassio_addons_stats" +DATA_SUPERVISOR_REPAIRS = "supervisor_repairs" HASSIO_UPDATE_INTERVAL = timedelta(minutes=5) ADDONS_COORDINATOR = "hassio_addons_coordinator" @@ -758,6 +760,10 @@ async def _async_setup_hardware_integration(hass): hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) ) + # Start listening for problems with supervisor and making repairs + hass.data[DATA_SUPERVISOR_REPAIRS] = repairs = SupervisorRepairs(hass, hassio) + await repairs.setup() + return True diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index e37a31ddbd6cf5..64ef7a718a5cbe 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -11,19 +11,26 @@ ATTR_DATA = "data" ATTR_DISCOVERY = "discovery" ATTR_ENABLE = "enable" +ATTR_ENDPOINT = "endpoint" ATTR_FOLDERS = "folders" +ATTR_HEALTHY = "healthy" ATTR_HOMEASSISTANT = "homeassistant" ATTR_INPUT = "input" +ATTR_METHOD = "method" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" +ATTR_RESULT = "result" +ATTR_SUPPORTED = "supported" +ATTR_TIMEOUT = "timeout" ATTR_TITLE = "title" +ATTR_UNHEALTHY = "unhealthy" +ATTR_UNHEALTHY_REASONS = "unhealthy_reasons" +ATTR_UNSUPPORTED = "unsupported" +ATTR_UNSUPPORTED_REASONS = "unsupported_reasons" +ATTR_UPDATE_KEY = "update_key" ATTR_USERNAME = "username" ATTR_UUID = "uuid" ATTR_WS_EVENT = "event" -ATTR_ENDPOINT = "endpoint" -ATTR_METHOD = "method" -ATTR_RESULT = "result" -ATTR_TIMEOUT = "timeout" X_AUTH_TOKEN = "X-Supervisor-Token" X_INGRESS_PATH = "X-Ingress-Path" @@ -38,6 +45,11 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe" EVENT_SUPERVISOR_EVENT = "supervisor_event" +EVENT_SUPERVISOR_UPDATE = "supervisor_update" +EVENT_HEALTH_CHANGED = "health_changed" +EVENT_SUPPORTED_CHANGED = "supported_changed" + +UPDATE_KEY_SUPERVISOR = "supervisor" ATTR_AUTO_UPDATE = "auto_update" ATTR_VERSION = "version" @@ -51,7 +63,6 @@ ATTR_URL = "url" ATTR_REPOSITORY = "repository" - DATA_KEY_ADDONS = "addons" DATA_KEY_OS = "os" DATA_KEY_SUPERVISOR = "supervisor" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7b3ed697227e77..ee16bdf815869b 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -190,6 +190,14 @@ def get_discovery_message(self, uuid): """ return self.send_command(f"/discovery/{uuid}", method="get") + @api_data + def get_resolution_info(self): + """Return data for Supervisor resolution center. + + This method return a coroutine. + """ + return self.send_command("/resolution/info", method="get") + @_api_bool async def update_hass_api(self, http_config, refresh_token): """Update Home Assistant API data on Hass.io.""" diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py new file mode 100644 index 00000000000000..a8c6788f4d5983 --- /dev/null +++ b/homeassistant/components/hassio/repairs.py @@ -0,0 +1,138 @@ +"""Supervisor events monitor.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.issue_registry import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) + +from .const import ( + ATTR_DATA, + ATTR_HEALTHY, + ATTR_SUPPORTED, + ATTR_UNHEALTHY, + ATTR_UNHEALTHY_REASONS, + ATTR_UNSUPPORTED, + ATTR_UNSUPPORTED_REASONS, + ATTR_UPDATE_KEY, + ATTR_WS_EVENT, + DOMAIN, + EVENT_HEALTH_CHANGED, + EVENT_SUPERVISOR_EVENT, + EVENT_SUPERVISOR_UPDATE, + EVENT_SUPPORTED_CHANGED, + UPDATE_KEY_SUPERVISOR, +) +from .handler import HassIO + +ISSUE_ID_UNHEALTHY = "unhealthy_system" +ISSUE_ID_UNSUPPORTED = "unsupported_system" + +INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy" +INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported" + + +class SupervisorRepairs: + """Create repairs from supervisor events.""" + + def __init__(self, hass: HomeAssistant, client: HassIO) -> None: + """Initialize supervisor repairs.""" + self._hass = hass + self._client = client + self._unsupported_reasons: set[str] = set() + self._unhealthy_reasons: set[str] = set() + + @property + def unhealthy_reasons(self) -> set[str]: + """Get unhealthy reasons. Returns empty set if system is healthy.""" + return self._unhealthy_reasons + + @unhealthy_reasons.setter + def unhealthy_reasons(self, reasons: set[str]) -> None: + """Set unhealthy reasons. Create or delete repairs as necessary.""" + for unhealthy in reasons - self.unhealthy_reasons: + async_create_issue( + self._hass, + DOMAIN, + f"{ISSUE_ID_UNHEALTHY}_{unhealthy}", + is_fixable=False, + learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}", + severity=IssueSeverity.CRITICAL, + translation_key="unhealthy", + translation_placeholders={"reason": unhealthy}, + ) + + for fixed in self.unhealthy_reasons - reasons: + async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNHEALTHY}_{fixed}") + + self._unhealthy_reasons = reasons + + @property + def unsupported_reasons(self) -> set[str]: + """Get unsupported reasons. Returns empty set if system is supported.""" + return self._unsupported_reasons + + @unsupported_reasons.setter + def unsupported_reasons(self, reasons: set[str]) -> None: + """Set unsupported reasons. Create or delete repairs as necessary.""" + for unsupported in reasons - self.unsupported_reasons: + async_create_issue( + self._hass, + DOMAIN, + f"{ISSUE_ID_UNSUPPORTED}_{unsupported}", + is_fixable=False, + learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}", + severity=IssueSeverity.WARNING, + translation_key="unsupported", + translation_placeholders={"reason": unsupported}, + ) + + for fixed in self.unsupported_reasons - reasons: + async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}") + + self._unsupported_reasons = reasons + + async def setup(self) -> None: + """Create supervisor events listener.""" + await self.update() + + async_dispatcher_connect( + self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_repairs + ) + + async def update(self) -> None: + """Update repairs from Supervisor resolution center.""" + data = await self._client.get_resolution_info() + self.unhealthy_reasons = set(data[ATTR_UNHEALTHY]) + self.unsupported_reasons = set(data[ATTR_UNSUPPORTED]) + + @callback + def _supervisor_events_to_repairs(self, event: dict[str, Any]) -> None: + """Create repairs from supervisor events.""" + if ATTR_WS_EVENT not in event: + return + + if ( + event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE + and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR + ): + self._hass.async_create_task(self.update()) + + elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED: + self.unhealthy_reasons = ( + set() + if event[ATTR_DATA][ATTR_HEALTHY] + else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS]) + ) + + elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED: + self.unsupported_reasons = ( + set() + if event[ATTR_DATA][ATTR_SUPPORTED] + else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS]) + ) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 90142bd453f0b9..81b5ce01b79f57 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -15,5 +15,15 @@ "update_channel": "Update Channel", "version_api": "Version API" } + }, + "issues": { + "unhealthy": { + "title": "Unhealthy system - {reason}", + "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it." + }, + "unsupported": { + "title": "Unsupported system - {reason}", + "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system." + } } } diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index a601f98f1c5fd5..c2dab178ad83bb 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -133,6 +133,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 1f915e17e616a0..9eaaf5f97d9913 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_diagnostics( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f0f94661d50805..371398e32c958a 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -183,6 +183,19 @@ def mock_all(aioclient_mock, request, os_info): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_setup_api_ping(hass, aioclient_mock): @@ -191,7 +204,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -230,7 +243,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -246,7 +259,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -258,7 +271,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -325,7 +338,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -339,7 +352,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -356,7 +369,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -426,14 +439,14 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][2] == "test" await hass.services.async_call("hassio", "host_shutdown", {}) await hass.services.async_call("hassio", "host_reboot", {}) await hass.async_block_till_done() - assert aioclient_mock.call_count == 11 + assert aioclient_mock.call_count == 12 await hass.services.async_call("hassio", "backup_full", {}) await hass.services.async_call( @@ -448,7 +461,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 13 + assert aioclient_mock.call_count == 14 assert aioclient_mock.mock_calls[-1][2] == { "homeassistant": True, "addons": ["test"], @@ -472,7 +485,7 @@ async def test_service_calls(hassio_env, hass, aioclient_mock, caplog): ) await hass.async_block_till_done() - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][2] == { "addons": ["test"], "folders": ["ssl"], @@ -491,12 +504,12 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): await hass.services.async_call("homeassistant", "stop") await hass.async_block_till_done() - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 await hass.services.async_call("homeassistant", "check_config") await hass.async_block_till_done() - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 with patch( "homeassistant.config.async_check_ha_config_file", return_value=None @@ -505,7 +518,7 @@ async def test_service_calls_core(hassio_env, hass, aioclient_mock): await hass.async_block_till_done() assert mock_check_config.called - assert aioclient_mock.call_count == 6 + assert aioclient_mock.call_count == 7 async def test_entry_load_and_unload(hass): @@ -758,7 +771,7 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): assert result await hass.async_block_till_done() - assert aioclient_mock.call_count == 15 + assert aioclient_mock.call_count == 16 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py new file mode 100644 index 00000000000000..ebaf46be3b597a --- /dev/null +++ b/tests/components/hassio/test_repairs.py @@ -0,0 +1,395 @@ +"""Test repairs from supervisor issues.""" + +from __future__ import annotations + +import os +from typing import Any +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .test_init import MOCK_ENVIRON + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture(autouse=True) +async def setup_repairs(hass): + """Set up the repairs integration.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock: AiohttpClientMocker, request: pytest.FixtureRequest): + """Mock all setup requests.""" + aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) + aioclient_mock.get("http://127.0.0.1/supervisor/ping", json={"result": "ok"}) + aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/info", + json={ + "result": "ok", + "data": { + "supervisor": "222", + "homeassistant": "0.110.0", + "hassos": "1.2.3", + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/host/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "data": { + "chassis": "vm", + "operating_system": "Debian GNU/Linux 10 (buster)", + "kernel": "4.19.0-6-amd64", + }, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/core/info", + json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, + ) + aioclient_mock.get( + "http://127.0.0.1/os/info", + json={ + "result": "ok", + "data": { + "version_latest": "1.0.0", + "version": "1.0.0", + "update_available": False, + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/supervisor/info", + json={ + "result": "ok", + "data": { + "result": "ok", + "version": "1.0.0", + "version_latest": "1.0.0", + "auto_update": True, + "addons": [], + }, + }, + ) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} + ) + aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + + +@pytest.fixture(autouse=True) +async def fixture_supervisor_environ(): + """Mock os environ for supervisor.""" + with patch.dict(os.environ, MOCK_ENVIRON): + yield + + +def mock_resolution_info( + aioclient_mock: AiohttpClientMocker, + unsupported: list[str] | None = None, + unhealthy: list[str] | None = None, +): + """Mock resolution/info endpoint with unsupported/unhealthy reasons.""" + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": unsupported or [], + "unhealthy": unhealthy or [], + "suggestions": [], + "issues": [], + "checks": [ + {"enabled": True, "slug": "supervisor_trust"}, + {"enabled": True, "slug": "free_space"}, + ], + }, + }, + ) + + +def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: str): + """Assert repair for unhealthy/unsupported in list.""" + repair_type = "unhealthy" if unhealthy else "unsupported" + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": f"{repair_type}_system_{reason}", + "issue_domain": None, + "learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}", + "severity": "critical" if unhealthy else "warning", + "translation_key": repair_type, + "translation_placeholders": { + "reason": reason, + }, + } in issues + + +async def test_unhealthy_repairs( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test repairs added for unhealthy systems.""" + mock_resolution_info(aioclient_mock, unhealthy=["docker", "setup"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup") + + +async def test_unsupported_repairs( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test repairs added for unsupported systems.""" + mock_resolution_info(aioclient_mock, unsupported=["content_trust", "os"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list( + msg["result"]["issues"], unhealthy=False, reason="content_trust" + ) + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + +async def test_unhealthy_repairs_add_remove( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test unhealthy repairs added and removed from dispatches.""" + mock_resolution_info(aioclient_mock) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "supervisor/event", + "data": { + "event": "health_changed", + "data": { + "healthy": False, + "unhealthy_reasons": ["docker"], + }, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + + await client.send_json( + { + "id": 3, + "type": "supervisor/event", + "data": { + "event": "health_changed", + "data": {"healthy": True}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_unsupported_repairs_add_remove( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test unsupported repairs added and removed from dispatches.""" + mock_resolution_info(aioclient_mock) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": "supervisor/event", + "data": { + "event": "supported_changed", + "data": { + "supported": False, + "unsupported_reasons": ["os"], + }, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + await client.send_json( + { + "id": 3, + "type": "supervisor/event", + "data": { + "event": "supported_changed", + "data": {"supported": True}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_reset_repairs_supervisor_restart( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Unsupported/unhealthy repairs reset on supervisor restart.""" + mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + aioclient_mock.clear_requests() + mock_resolution_info(aioclient_mock) + await client.send_json( + { + "id": 2, + "type": "supervisor/event", + "data": { + "event": "supervisor_update", + "update_key": "supervisor", + "data": {}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_reasons_added_and_removed( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Test an unsupported/unhealthy reasons being added and removed at same time.""" + mock_resolution_info(aioclient_mock, unsupported=["os"], unhealthy=["docker"]) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="docker") + assert_repair_in_list(msg["result"]["issues"], unhealthy=False, reason="os") + + aioclient_mock.clear_requests() + mock_resolution_info( + aioclient_mock, unsupported=["content_trust"], unhealthy=["setup"] + ) + await client.send_json( + { + "id": 2, + "type": "supervisor/event", + "data": { + "event": "supervisor_update", + "update_key": "supervisor", + "data": {}, + }, + } + ) + msg = await client.receive_json() + assert msg["success"] + await hass.async_block_till_done() + + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="setup") + assert_repair_in_list( + msg["result"]["issues"], unhealthy=False, reason="content_trust" + ) diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 16cce09b800a76..e9f0bd631b0679 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -126,6 +126,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index aaa77cde129d9e..02d6b1dbf6bc4a 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -139,6 +139,19 @@ def mock_all(aioclient_mock, request): "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) aioclient_mock.post("http://127.0.0.1/refresh_updates", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) @pytest.mark.parametrize( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index 5d11d13166e58b..767f0abaf35ea0 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -61,6 +61,19 @@ def mock_all(aioclient_mock): aioclient_mock.get( "http://127.0.0.1/ingress/panels", json={"result": "ok", "data": {"panels": {}}} ) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) async def test_ws_subscription(hassio_env, hass: HomeAssistant, hass_ws_client): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 7a4202c1a674cb..a4249a1efb618b 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -198,7 +198,17 @@ async def unauth_handler(request): manager: IpBanManager = app[KEY_BAN_MANAGER] - assert await async_setup_component(hass, "hassio", {"hassio": {}}) + with patch( + "homeassistant.components.hassio.HassIO.get_resolution_info", + return_value={ + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + ): + assert await async_setup_component(hass, "hassio", {"hassio": {}}) m_open = mock_open() diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 204eb6bf77291b..40d889185ddbaf 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -57,6 +57,19 @@ async def mock_supervisor_fixture(hass, aioclient_mock): """Mock supervisor.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [], + "checks": [], + }, + }, + ) with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", return_value=True, From 7046f5f19e3238c0b3111b9712aad4245ea9c759 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 1 Nov 2022 02:22:21 +0100 Subject: [PATCH 114/394] Only try initializing Hue motion LED on endpoint 2 with ZHA (#81205) --- homeassistant/components/zha/core/channels/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index ded51455af817b..c028a6021da9c5 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -156,7 +156,7 @@ class BasicChannel(ZigbeeChannel): def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize Basic channel.""" super().__init__(cluster, ch_pool) - if is_hue_motion_sensor(self): + if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2: self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name self.ZCL_INIT_ATTRS.copy() ) From 9b4f2df8f34355099c8b44cc041922e7d57b6016 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 12:29:12 -0500 Subject: [PATCH 115/394] Bump aiohomekit to 2.2.10 (#81312) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 58e258294a02ec..93aae62daab91e 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.9"], + "requirements": ["aiohomekit==2.2.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index bfa8a46021cae3..c8b0aec9ccc6c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.9 +aiohomekit==2.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6fbc1a59d745c9..1e0053ae72d573 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.9 +aiohomekit==2.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http From d7e76fdf3a5e89617d79508bd3418459197c4c6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 12:35:43 -0500 Subject: [PATCH 116/394] Bump zeroconf to 0.39.4 (#81313) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 967dd761ac7977..382cf42b54fde7 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.39.3"], + "requirements": ["zeroconf==0.39.4"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e2c57d87c19c03..411b0a06646a27 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -42,7 +42,7 @@ typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.8.1 -zeroconf==0.39.3 +zeroconf==0.39.4 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index c8b0aec9ccc6c6..737191c0674ba7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2604,7 +2604,7 @@ zamg==0.1.1 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.39.3 +zeroconf==0.39.4 # homeassistant.components.zha zha-quirks==0.0.84 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e0053ae72d573..2d4c36b717a28d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1805,7 +1805,7 @@ youless-api==0.16 zamg==0.1.1 # homeassistant.components.zeroconf -zeroconf==0.39.3 +zeroconf==0.39.4 # homeassistant.components.zha zha-quirks==0.0.84 From 19a5c87da6869244e4b66a7efe544976dee45955 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 13:38:57 -0500 Subject: [PATCH 117/394] Bump oralb-ble to 0.10.0 (#81315) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 8f6949468048c0..cad6167228cce4 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.9.0"], + "requirements": ["oralb-ble==0.10.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 737191c0674ba7..25f63a9faf73ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1238,7 +1238,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.9.0 +oralb-ble==0.10.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d4c36b717a28d..bc3f4ee9afae59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -883,7 +883,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.9.0 +oralb-ble==0.10.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 356953c8bc898770b22fcaa0522e3a83eeece68a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 31 Oct 2022 20:36:59 +0100 Subject: [PATCH 118/394] Update base image to 2022.10.0 (#81317) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 9cf66e2621a073..14a59641388ba2 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.10.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.10.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.10.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.10.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.10.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 882ad31a99f2f0e306e7b902c28837d72028d7b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 20:21:40 -0500 Subject: [PATCH 119/394] Fix Yale Access Bluetooth not being available again after being unavailable (#81320) --- homeassistant/components/yalexs_ble/__init__.py | 13 +++++++++++++ homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 6073bf7a0322c5..7a2b3146265f4d 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -94,6 +94,19 @@ def _async_update_ble( entry.title, push_lock ) + @callback + def _async_device_unavailable( + _service_info: bluetooth.BluetoothServiceInfoBleak, + ) -> None: + """Handle device not longer being seen by the bluetooth stack.""" + push_lock.reset_advertisement_state() + + entry.async_on_unload( + bluetooth.async_track_unavailable( + hass, _async_device_unavailable, push_lock.address + ) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 7bc8bde5b30aa0..b43ce18a7e9d76 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.9.4"], + "requirements": ["yalexs-ble==1.9.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index 25f63a9faf73ba..b6d58a7854b64d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2577,7 +2577,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.4 +yalexs-ble==1.9.5 # homeassistant.components.august yalexs==1.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc3f4ee9afae59..dcb073c2a7ce9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1787,7 +1787,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.9.4 +yalexs-ble==1.9.5 # homeassistant.components.august yalexs==1.2.6 From 599c23c1d72ad460546ab6707e045b20d0ffd720 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 31 Oct 2022 20:42:18 +0100 Subject: [PATCH 120/394] Update frontend to 20221031.0 (#81324) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index c8d3645435f0d9..aed26eb5de1ea4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221027.0"], + "requirements": ["home-assistant-frontend==20221031.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 411b0a06646a27..adff342729de81 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.60.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index b6d58a7854b64d..5ad8830149c016 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dcb073c2a7ce9c..8476aab8c2449c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221027.0 +home-assistant-frontend==20221031.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 941512641b1f538f6ed9333a3184fe9839f6205d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Oct 2022 20:21:11 -0500 Subject: [PATCH 121/394] Improve esphome bluetooth error reporting (#81326) --- homeassistant/components/esphome/bluetooth/client.py | 10 ++++++++-- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 5f20a73f4d6316..72531a2503a88c 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -7,7 +7,11 @@ from typing import Any, TypeVar, cast import uuid -from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError +from aioesphomeapi import ( + ESP_CONNECTION_ERROR_DESCRIPTION, + ESPHOME_GATT_ERRORS, + BLEConnectionError, +) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic @@ -207,7 +211,9 @@ def _on_bluetooth_connection_state( human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error] except (KeyError, ValueError): ble_connection_error_name = str(error) - human_error = f"Unknown error code {error}" + human_error = ESPHOME_GATT_ERRORS.get( + error, f"Unknown error code {error}" + ) connected_future.set_exception( BleakError( f"Error {ble_connection_error_name} while connecting: {human_error}" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index c27e3b8dc3e114..64cd6b4029c6aa 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==11.4.1"], + "requirements": ["aioesphomeapi==11.4.2"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 5ad8830149c016..662bc2b507562b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -153,7 +153,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.1 +aioesphomeapi==11.4.2 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8476aab8c2449c..be8b850e6b4651 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.1 +aioesphomeapi==11.4.2 # homeassistant.components.flo aioflo==2021.11.0 From 0ac0e9c0d5ecac6a8a64cd10e34daff13ae1db53 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 31 Oct 2022 21:23:21 -0400 Subject: [PATCH 122/394] Bumped version to 2022.11.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5acf294fb68112..f547e536ae00a2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 16ff4bc6bbef85..5f2aa4d4311f09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b4" +version = "2022.11.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ed2696f03e40c2175bf42c6ad315c4fc72e51071 Mon Sep 17 00:00:00 2001 From: Willem-Jan van Rootselaar Date: Tue, 1 Nov 2022 09:28:02 +0100 Subject: [PATCH 123/394] Bump python-bsblan to version 0.5.7 (#81330) --- homeassistant/components/bsblan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index e2b0e5bebfaf99..87fb7f8c08f67d 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -3,7 +3,7 @@ "name": "BSB-Lan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bsblan", - "requirements": ["python-bsblan==0.5.6"], + "requirements": ["python-bsblan==0.5.7"], "codeowners": ["@liudger"], "iot_class": "local_polling", "loggers": ["bsblan"] diff --git a/requirements_all.txt b/requirements_all.txt index 5b5b94d966b2ee..d0f0572b9f56dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1953,7 +1953,7 @@ pythinkingcleaner==0.0.3 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.6 +python-bsblan==0.5.7 # homeassistant.components.clementine python-clementine-remote==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a949ae7d13ec7..f18b83b13fe108 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1373,7 +1373,7 @@ pytankerkoenig==0.0.6 pytautulli==21.11.0 # homeassistant.components.bsblan -python-bsblan==0.5.6 +python-bsblan==0.5.7 # homeassistant.components.ecobee python-ecobee-api==0.2.14 From 16beed25654d4bd0829b9020ffa1fa6837784216 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Nov 2022 09:29:38 +0100 Subject: [PATCH 124/394] Always use Celsius in Shelly integration (#80842) --- homeassistant/components/shelly/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 921ffb352d57b1..b65c314789a30c 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -16,7 +16,6 @@ from homeassistant.helpers import aiohttp_client, device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( CONF_COAP_PORT, @@ -113,13 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly block based device from a config entry.""" - temperature_unit = "C" if hass.config.units is METRIC_SYSTEM else "F" - options = aioshelly.common.ConnectionOptions( entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), - temperature_unit, ) coap_context = await get_coap_context(hass) From 2b935564c2cbe139e4591e5ca21eebdc28022b58 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 1 Nov 2022 02:08:36 -0700 Subject: [PATCH 125/394] Bump gcal_sync to 2.2.2 and fix recurring event bug (#81339) * Bump gcal_sync to 2.2.2 and fix recurring event bug * Bump to 2.2.2 --- 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 ce95e3112ee91b..9a184bdd636a2e 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==2.2.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==2.2.2", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index d0f0572b9f56dd..e16974be627007 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -728,7 +728,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.0 +gcal-sync==2.2.2 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f18b83b13fe108..c48ecf6bbd26a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -544,7 +544,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.0 +gcal-sync==2.2.2 # homeassistant.components.geocaching geocachingapi==0.2.1 From 3afef1f8fe65ad6c13efcea85425837360c4a466 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 1 Nov 2022 10:10:30 +0100 Subject: [PATCH 126/394] Add task id attribute to fireservicerota sensor (#81323) --- homeassistant/components/fireservicerota/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 36455da9fb7d0f..1484ff7f1543a1 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -79,6 +79,7 @@ def extra_state_attributes(self) -> dict[str, Any]: "type", "responder_mode", "can_respond_until", + "task_ids", ): if data.get(value): attr[value] = data[value] From 9dd6d5d0abe21445d985c5d2fd01ff2b6d1a67d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 10:17:01 +0100 Subject: [PATCH 127/394] Fix power/energy mixup in Youless (#81345) --- homeassistant/components/youless/sensor.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 19e9c635dce017..53ffb22393917a 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -39,13 +39,13 @@ async def async_setup_entry( async_add_entities( [ GasSensor(coordinator, device), - PowerMeterSensor( + EnergyMeterSensor( coordinator, device, "low", SensorStateClass.TOTAL_INCREASING ), - PowerMeterSensor( + EnergyMeterSensor( coordinator, device, "high", SensorStateClass.TOTAL_INCREASING ), - PowerMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL), + EnergyMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL), CurrentPowerSensor(coordinator, device), DeliveryMeterSensor(coordinator, device, "low"), DeliveryMeterSensor(coordinator, device, "high"), @@ -68,10 +68,6 @@ def __init__( ) -> None: """Create the sensor.""" super().__init__(coordinator) - self._device = device - self._device_group = device_group - self._sensor_id = sensor_id - self._attr_unique_id = f"{DOMAIN}_{device}_{sensor_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{device}_{device_group}")}, @@ -149,10 +145,10 @@ def __init__( ) -> None: """Instantiate a delivery meter sensor.""" super().__init__( - coordinator, device, "delivery", "Power delivery", f"delivery_{dev_type}" + coordinator, device, "delivery", "Energy delivery", f"delivery_{dev_type}" ) self._type = dev_type - self._attr_name = f"Power delivery {dev_type}" + self._attr_name = f"Energy delivery {dev_type}" @property def get_sensor(self) -> YoulessSensor | None: @@ -163,7 +159,7 @@ def get_sensor(self) -> YoulessSensor | None: return getattr(self.coordinator.data.delivery_meter, f"_{self._type}", None) -class PowerMeterSensor(YoulessBaseSensor): +class EnergyMeterSensor(YoulessBaseSensor): """The Youless low meter value sensor.""" _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR @@ -177,13 +173,13 @@ def __init__( dev_type: str, state_class: SensorStateClass, ) -> None: - """Instantiate a power meter sensor.""" + """Instantiate a energy meter sensor.""" super().__init__( - coordinator, device, "power", "Power usage", f"power_{dev_type}" + coordinator, device, "power", "Energy usage", f"power_{dev_type}" ) self._device = device self._type = dev_type - self._attr_name = f"Power {dev_type}" + self._attr_name = f"Energy {dev_type}" self._attr_state_class = state_class @property From e22f69ea8c7921c9f1351a916ca6561c3913754d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 10:34:44 +0100 Subject: [PATCH 128/394] Update Pillow to 9.3.0 (#81343) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 766167b7af9678..39ed9c552bbdc8 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.2.0"], + "requirements": ["pydoods==1.0.2", "pillow==9.3.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 83b34f73dc844f..938e685dcab2bd 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["ha-av==10.0.0", "pillow==9.2.0"], + "requirements": ["ha-av==10.0.0", "pillow==9.3.0"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index ed500c89011843..888d6fc1fab3fb 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==9.2.0"], + "requirements": ["pillow==9.3.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 59d19bc785e730..e863122b872477 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.2.0"], + "requirements": ["pillow==9.3.0"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 666cc0c93cea01..1a394e17f293cf 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.2.0", "pyzbar==0.1.7"], + "requirements": ["pillow==9.3.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated", "loggers": ["pyzbar"] diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 09457fc4ca5adb..cfe834bd648ded 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.2.0"], + "requirements": ["pillow==9.3.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 400664079e266d..042a642429f298 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.2.0", "simplehound==0.3"], + "requirements": ["pillow==9.3.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling", "loggers": ["simplehound"] diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9f719b7e1b31b0..ef02f208e8c7c4 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.23.2", - "pillow==9.2.0" + "pillow==9.3.0" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index adff342729de81..3a2f4b5a7f84ab 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ jinja2==3.1.2 lru-dict==1.1.8 orjson==3.8.1 paho-mqtt==1.6.1 -pillow==9.2.0 +pillow==9.3.0 pip>=21.0,<22.4 psutil-home-assistant==0.0.1 pyserial==3.5 diff --git a/requirements_all.txt b/requirements_all.txt index e16974be627007..ee39f1fc66d1ea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1300,7 +1300,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.2.0 +pillow==9.3.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c48ecf6bbd26a3..9ad0ce653c6679 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -930,7 +930,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.2.0 +pillow==9.3.0 # homeassistant.components.plex plexapi==4.13.0 From f5f96535ad1ae00df83d8f4b5dcf6f5a592f979f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 1 Nov 2022 12:53:44 +0200 Subject: [PATCH 129/394] Bump aioshelly to 4.1.2 (#81342) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 07499ce1e9d5f5..70970e73e307ae 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==4.1.1"], + "requirements": ["aioshelly==4.1.2"], "dependencies": ["http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index ee39f1fc66d1ea..0df45788f9eaae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.1 +aioshelly==4.1.2 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ad0ce653c6679..2732638cedb1a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.1 +aioshelly==4.1.2 # homeassistant.components.skybell aioskybell==22.7.0 From 514f619cff20bb1e58db7a05e0bf0514d00ec5b6 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:51:20 +0100 Subject: [PATCH 130/394] Tuya configuration for `tuya_manufacturer` cluster (#81311) * Tuya configuration for tuya_manufacturer cluster * fix codespell * Add attributes initialization * Fix pylint complaints --- .../zha/core/channels/manufacturerspecific.py | 35 +++++++++++ .../components/zha/core/registries.py | 1 + homeassistant/components/zha/select.py | 59 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 5139854d66abbf..814e7700d01e04 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -59,6 +59,41 @@ class PhillipsRemote(ZigbeeChannel): REPORT_CONFIG = () +@registries.CHANNEL_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.TUYA_MANUFACTURER_CLUSTER) +class TuyaChannel(ZigbeeChannel): + """Channel for the Tuya manufacturer Zigbee cluster.""" + + REPORT_CONFIG = () + + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: + """Initialize TuyaChannel.""" + super().__init__(cluster, ch_pool) + + if self.cluster.endpoint.manufacturer in ( + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + ): + self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + "backlight_mode": True, + "power_on_state": True, + } + + @registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0) @registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0) class OppleRemote(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 2480cf1cd43a20..42f6bb55f5199a 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -33,6 +33,7 @@ SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 +TUYA_MANUFACTURER_CLUSTER = 0xEF00 VOC_LEVEL_CLUSTER = 0x042E REMOTE_DEVICE_TYPES = { diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 38f2f417643364..5ac0ec6d16408f 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -240,6 +240,27 @@ class TuyaPowerOnState(types.enum8): channel_names=CHANNEL_ON_OFF, models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"}, ) +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + }, +) class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_state"): """Representation of a ZHA power on state select entity.""" @@ -248,6 +269,44 @@ class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_stat _attr_name = "Power on state" +class MoesBacklightMode(types.enum8): + """MOES switch backlight mode enum.""" + + Off = 0x00 + LightWhenOn = 0x01 + LightWhenOff = 0x02 + Freeze = 0x03 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + }, +) +class MoesBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"): + """Moes devices have a different backlight mode select options.""" + + _select_attr = "backlight_mode" + _enum = MoesBacklightMode + _attr_name = "Backlight mode" + + class AqaraMotionSensitivities(types.enum8): """Aqara motion sensitivities.""" From 5f1c92ce51e5138e2a1bc5c2c581b2c053518659 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 1 Nov 2022 09:06:55 -0400 Subject: [PATCH 131/394] Fix individual LED range for ZHA device action (#81351) The inovelli individual LED effect device action can address 7 LEDs. I had set the range 1-7 but it should be 0-6. --- homeassistant/components/zha/device_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 1cb988b1c1513f..3e2a3591c804cf 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -93,7 +93,7 @@ ), INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema( { - vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)), + vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), vol.Required("effect_type"): vol.In( InovelliConfigEntityChannel.LEDEffectType.__members__.keys() ), From 8d50b05d0d9058abb21ac2fcc0f4631d39a0578c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 1 Nov 2022 14:30:42 +0100 Subject: [PATCH 132/394] Add ability to set device class on knx sensor (#81278) Add ability to set device class on sensor --- homeassistant/components/knx/schema.py | 7 ++++++- homeassistant/components/knx/sensor.py | 21 +++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index c1615b7e8e2b59..13f6f153dafeb5 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -21,7 +21,11 @@ DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) from homeassistant.components.number import NumberMode -from homeassistant.components.sensor import CONF_STATE_CLASS, STATE_CLASSES_SCHEMA +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, + STATE_CLASSES_SCHEMA, +) from homeassistant.components.switch import ( DEVICE_CLASSES_SCHEMA as SWITCH_DEVICE_CLASSES_SCHEMA, ) @@ -855,6 +859,7 @@ class SensorSchema(KNXPlatformSchema): vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, vol.Required(CONF_TYPE): sensor_type_validator, vol.Required(CONF_STATE_ADDRESS): ga_list_validator, + vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, } ) diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index ceb9f435d83edc..20bbddf14a1c07 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -12,7 +12,13 @@ DEVICE_CLASSES, SensorEntity, ) -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, CONF_TYPE, Platform +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_NAME, + CONF_TYPE, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, StateType @@ -54,11 +60,14 @@ class KNXSensor(KnxEntity, SensorEntity): def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" super().__init__(_create_sensor(xknx, config)) - self._attr_device_class = ( - self._device.ha_device_class() - if self._device.ha_device_class() in DEVICE_CLASSES - else None - ) + if device_class := config.get(CONF_DEVICE_CLASS): + self._attr_device_class = device_class + else: + self._attr_device_class = ( + self._device.ha_device_class() + if self._device.ha_device_class() in DEVICE_CLASSES + else None + ) self._attr_force_update = self._device.always_callback self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) self._attr_unique_id = str(self._device.sensor_value.group_address_state) From 509d5fd69d09cff311750420879fc18a02955bf1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Nov 2022 15:57:48 +0100 Subject: [PATCH 133/394] Lower log level for non-JSON payload in MQTT update (#81348) Change log level --- homeassistant/components/mqtt/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 986ad013520205..5536d16d1c77eb 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -181,9 +181,9 @@ def handle_state_message_received(msg: ReceiveMessage) -> None: msg.topic, ) except JSON_DECODE_EXCEPTIONS: - _LOGGER.warning( + _LOGGER.debug( "No valid (JSON) payload detected after processing payload '%s' on topic %s", - json_payload, + payload, msg.topic, ) json_payload["installed_version"] = payload From db0785827f6bab45de7d6f5860aaedeeb64d3b94 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 1 Nov 2022 16:25:01 +0100 Subject: [PATCH 134/394] Revert "Do not write state if payload is `''`" for MQTT sensor (#81347) * Revert "Do not write state if payload is ''" This reverts commit 869c11884e2b06d5f5cb5a8a4f78247a6972149e. * Add test --- homeassistant/components/mqtt/sensor.py | 4 ++-- tests/components/mqtt/test_sensor.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index d95d669e72f6cf..52ba1a7e3c28e1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -271,8 +271,8 @@ def _update_state(msg): ) elif self.device_class == SensorDeviceClass.DATE: payload = payload.date() - if payload != "": - self._state = payload + + self._state = payload def _update_last_reset(msg): payload = self._last_reset_template(msg.payload) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 6cfaa9678bba3d..1884d04efc3073 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -313,6 +313,12 @@ async def test_setting_sensor_value_via_mqtt_json_message( assert state.state == "100" + # Make sure the state is written when a sensor value is reset to '' + async_fire_mqtt_message(hass, "test-topic", '{ "val": "" }') + state = hass.states.get("sensor.test") + + assert state.state == "" + async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state( hass, mqtt_mock_entry_with_yaml_config From 9be204629bb4e11a4f385bd003cac9fb46d76e76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 11:44:58 -0500 Subject: [PATCH 135/394] Bump aiohomekit to 2.2.11 (#81358) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 93aae62daab91e..6533d7f29beaa7 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.10"], + "requirements": ["aiohomekit==2.2.11"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 0df45788f9eaae..aea2dbd2563705 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.10 +aiohomekit==2.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2732638cedb1a2..80ef0d3228e972 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.10 +aiohomekit==2.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http From 5b09ab93dcd50026b3b4c33dd920dc788ee77adf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 12:07:03 -0500 Subject: [PATCH 136/394] Immediately prefer advertisements from alternate sources when a scanner goes away (#81357) --- homeassistant/components/bluetooth/manager.py | 5 + tests/components/bluetooth/test_manager.py | 92 +++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index c3a0e0998f1919..d29023acef78ea 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -127,6 +127,7 @@ def __init__( self._non_connectable_scanners: list[BaseHaScanner] = [] self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} + self._sources: set[str] = set() @property def supports_passive_scan(self) -> bool: @@ -379,6 +380,7 @@ def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: if ( (old_service_info := all_history.get(address)) and source != old_service_info.source + and old_service_info.source in self._sources and self._prefer_previous_adv_from_different_source( old_service_info, service_info ) @@ -398,6 +400,7 @@ def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: # the old connectable advertisement or ( source != old_connectable_service_info.source + and old_connectable_service_info.source in self._sources and self._prefer_previous_adv_from_different_source( old_connectable_service_info, service_info ) @@ -597,8 +600,10 @@ def async_register_scanner( def _unregister_scanner() -> None: self._advertisement_tracker.async_remove_source(scanner.source) scanners.remove(scanner) + self._sources.remove(scanner.source) scanners.append(scanner) + self._sources.add(scanner.source) return _unregister_scanner @hass_callback diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index c6a65046ef905c..0375f68309f717 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -5,11 +5,14 @@ from bleak.backends.scanner import BLEDevice from bluetooth_adapters import AdvertisementHistory +import pytest from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import models from homeassistant.components.bluetooth.manager import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import ( @@ -20,8 +23,28 @@ ) +@pytest.fixture +def register_hci0_scanner(hass: HomeAssistant) -> None: + """Register an hci0 scanner.""" + cancel = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci0"), True + ) + yield + cancel() + + +@pytest.fixture +def register_hci1_scanner(hass: HomeAssistant) -> None: + """Register an hci1 scanner.""" + cancel = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci1"), True + ) + yield + cancel() + + async def test_advertisements_do_not_switch_adapters_for_no_reason( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test we only switch adapters when needed.""" @@ -68,7 +91,9 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason( ) -async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): +async def test_switching_adapters_based_on_rssi( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on rssi.""" address = "44:44:33:11:23:45" @@ -122,7 +147,9 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): ) -async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): +async def test_switching_adapters_based_on_zero_rssi( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on zero rssi.""" address = "44:44:33:11:23:45" @@ -176,7 +203,9 @@ async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): ) -async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): +async def test_switching_adapters_based_on_stale( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on the previous advertisement being stale.""" address = "44:44:33:11:23:41" @@ -256,7 +285,7 @@ async def test_restore_history_from_dbus(hass, one_adapter): async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test switching adapters based on rssi from connectable to non connectable.""" @@ -339,7 +368,7 @@ async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_connectable( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test we can still get a connectable BLEDevice when the best path is non-connectable. @@ -384,3 +413,54 @@ async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_ bluetooth.async_ble_device_from_address(hass, address, True) is switchbot_device_poor_signal ) + + +async def test_switching_adapters_when_one_goes_away( + hass, enable_bluetooth, register_hci0_scanner +): + """Test switching adapters when one goes away.""" + cancel_hci2 = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci2"), True + ) + + address = "44:44:33:11:23:45" + + switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal") + switchbot_adv_good_signal = generate_advertisement_data( + local_name="wohand_good_signal", service_uuids=[], rssi=-60 + ) + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci2" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal") + switchbot_adv_poor_signal = generate_advertisement_data( + local_name="wohand_poor_signal", service_uuids=[], rssi=-100 + ) + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # We want to prefer the good signal when we have options + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + cancel_hci2() + + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # Now that hci2 is gone, we should prefer the poor signal + # since no poor signal is better than no signal + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal + ) From 972b36b469d0212f3de2231b8e5f0e5c2029f63d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 12:07:42 -0500 Subject: [PATCH 137/394] Adjust time to remove stale connectable devices from the esphome ble to closer match bluez (#81356) --- .../components/esphome/bluetooth/scanner.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 7c8064d5583d04..4fbaf7cabb6e0e 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -6,6 +6,7 @@ from datetime import timedelta import re import time +from typing import Final from aioesphomeapi import BluetoothLEAdvertisement from bleak.backends.device import BLEDevice @@ -23,6 +24,15 @@ TWO_CHAR = re.compile("..") +# The maximum time between advertisements for a device to be considered +# stale when the advertisement tracker can determine the interval for +# connectable devices. +# +# BlueZ uses 180 seconds by default but we give it a bit more time +# to account for the esp32's bluetooth stack being a bit slower +# than BlueZ's. +CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 195 + class ESPHomeScanner(BaseHaScanner): """Scanner for esphome.""" @@ -45,8 +55,12 @@ def __init__( self._connector = connector self._connectable = connectable self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} + self._fallback_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS if connectable: self._details["connector"] = connector + self._fallback_seconds = ( + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) @callback def async_setup(self) -> CALLBACK_TYPE: @@ -61,7 +75,7 @@ def _async_expire_devices(self, _datetime: datetime.datetime) -> None: expired = [ address for address, timestamp in self._discovered_device_timestamps.items() - if now - timestamp > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + if now - timestamp > self._fallback_seconds ] for address in expired: del self._discovered_device_advertisement_datas[address] From dfe399e370b432ba5936e629d3d40f1227849a21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 18:08:26 +0100 Subject: [PATCH 138/394] Cherry-pick translation updates for Supervisor (#81341) --- homeassistant/components/hassio/translations/ca.json | 10 ++++++++++ homeassistant/components/hassio/translations/en.json | 10 ++++++++++ homeassistant/components/hassio/translations/es.json | 10 ++++++++++ homeassistant/components/hassio/translations/et.json | 10 ++++++++++ homeassistant/components/hassio/translations/hu.json | 10 ++++++++++ .../components/hassio/translations/pt-BR.json | 10 ++++++++++ homeassistant/components/hassio/translations/ru.json | 10 ++++++++++ 7 files changed, 70 insertions(+) diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 2c4285d49084f4..14679301993a1f 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.", + "title": "Sistema no saludable - {reason}" + }, + "unsupported": { + "description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.", + "title": "Sistema no compatible - {reason}" + } + }, "system_health": { "info": { "agent_version": "Versi\u00f3 de l'agent", diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 14d79f0d8d6c8c..b6f006e30932c5 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it.", + "title": "Unhealthy system - {reason}" + }, + "unsupported": { + "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system.", + "title": "Unsupported system - {reason}" + } + }, "system_health": { "info": { "agent_version": "Agent Version", diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 102256ef1173f3..f2aef9d7214b73 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "Actualmente el sistema no est\u00e1 en buen estado debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que est\u00e1 mal y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado: {reason}" + }, + "unsupported": { + "description": "El sistema no es compatible debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que esto significa y c\u00f3mo volver a un sistema compatible.", + "title": "Sistema no compatible: {reason}" + } + }, "system_health": { "info": { "agent_version": "Versi\u00f3n del agente", diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index b86eef353b962b..ea0f78c0c57c0d 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "S\u00fcsteem ei ole praegu korras '{reason}' t\u00f5ttu. Kasuta linki, et saada rohkem teavet selle kohta, mis on valesti ja kuidas seda parandada.", + "title": "Vigane s\u00fcsteem \u2013 {reason}" + }, + "unsupported": { + "description": "S\u00fcsteemi ei toetata '{reason}' t\u00f5ttu. Kasuta linki, et saada lisateavet selle kohta, mida see t\u00e4hendab ja kuidas toetatud s\u00fcsteemi naasta.", + "title": "Toetamata s\u00fcsteem \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "Agendi versioon", diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 4c83b94935d43b..604a8ae59e63f1 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "A rendszer jelenleg renellenes \u00e1llapotban van '{reason}' miatt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet is megtudhat arr\u00f3l, hogy mi a probl\u00e9ma, \u00e9s hogyan jav\u00edthatja ki.", + "title": "Rendellenes \u00e1llapot \u2013 {reason}" + }, + "unsupported": { + "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: '{reason}'. A hivatkoz\u00e1s seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat arr\u00f3l, mit jelent ez, \u00e9s hogyan t\u00e9rhet vissza egy t\u00e1mogatott rendszerhez.", + "title": "Nem t\u00e1mogatott rendszer \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "\u00dcgyn\u00f6k verzi\u00f3", diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json index 4f3e5d84ec1699..47e0b6df4aed6b 100644 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a '{reason}'. Use o link para saber mais sobre o que est\u00e1 errado e como corrigi-lo.", + "title": "Sistema insalubre - {reason}" + }, + "unsupported": { + "description": "O sistema n\u00e3o \u00e9 suportado devido a '{reason}'. Use o link para saber mais sobre o que isso significa e como retornar a um sistema compat\u00edvel.", + "title": "Sistema n\u00e3o suportado - {reason}" + } + }, "system_health": { "info": { "agent_version": "Vers\u00e3o do Agent", diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 5e1caa41ebf56c..0ab366c1775ef0 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + }, + "unsupported": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0438 \u043a\u0430\u043a \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + } + }, "system_health": { "info": { "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430", From 8965a1322cd74fc3d77d38eefc134f81db1a95d9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Nov 2022 09:29:38 +0100 Subject: [PATCH 139/394] Always use Celsius in Shelly integration (#80842) --- homeassistant/components/shelly/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 921ffb352d57b1..b65c314789a30c 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -16,7 +16,6 @@ from homeassistant.helpers import aiohttp_client, device_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( CONF_COAP_PORT, @@ -113,13 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly block based device from a config entry.""" - temperature_unit = "C" if hass.config.units is METRIC_SYSTEM else "F" - options = aioshelly.common.ConnectionOptions( entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), - temperature_unit, ) coap_context = await get_coap_context(hass) From 9b87f7f6f9409e1117c255c011f7480daf17d9cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Oct 2022 15:50:59 -0500 Subject: [PATCH 140/394] Fix homekit diagnostics test when version changes (#81046) --- tests/components/homekit/test_diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 15d4a6f6e2eabc..1f6f7c584f3ee6 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -190,7 +190,7 @@ async def test_config_entry_accessory( "iid": 7, "perms": ["pr"], "type": "52", - "value": "2022.11.0", + "value": ANY, }, ], "iid": 1, From 4684101a853baeb8bb7624136748fe094c1cb980 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 28 Oct 2022 17:05:43 +0200 Subject: [PATCH 141/394] Improve MQTT update platform (#81131) * Allow JSON as state_topic payload * Add title * Add release_url * Add release_summary * Add entity_picture * Fix typo * Add abbreviations --- .../components/mqtt/abbreviations.py | 4 + homeassistant/components/mqtt/update.py | 82 +++++++++-- tests/components/mqtt/test_update.py | 134 +++++++++++++++++- 3 files changed, 211 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 67fffec1106b4b..00f6d3575536f4 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -52,6 +52,7 @@ "e": "encoding", "en": "enabled_by_default", "ent_cat": "entity_category", + "ent_pic": "entity_picture", "err_t": "error_topic", "err_tpl": "error_template", "fanspd_t": "fan_speed_topic", @@ -169,6 +170,8 @@ "pr_mode_val_tpl": "preset_mode_value_template", "pr_modes": "preset_modes", "r_tpl": "red_template", + "rel_s": "release_summary", + "rel_u": "release_url", "ret": "retain", "rgb_cmd_tpl": "rgb_command_template", "rgb_cmd_t": "rgb_command_topic", @@ -242,6 +245,7 @@ "tilt_opt": "tilt_optimistic", "tilt_status_t": "tilt_status_topic", "tilt_status_tpl": "tilt_status_template", + "tit": "title", "t": "topic", "uniq_id": "unique_id", "unit_of_meas": "unit_of_measurement", diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 8fdc6393e0b42e..986ad013520205 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -19,6 +19,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -30,6 +31,7 @@ CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + PAYLOAD_EMPTY_JSON, ) from .debug_info import log_messages from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper @@ -40,20 +42,28 @@ DEFAULT_NAME = "MQTT Update" +CONF_ENTITY_PICTURE = "entity_picture" CONF_LATEST_VERSION_TEMPLATE = "latest_version_template" CONF_LATEST_VERSION_TOPIC = "latest_version_topic" CONF_PAYLOAD_INSTALL = "payload_install" +CONF_RELEASE_SUMMARY = "release_summary" +CONF_RELEASE_URL = "release_url" +CONF_TITLE = "title" PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_ENTITY_PICTURE): cv.string, vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template, - vol.Required(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_INSTALL): cv.string, + vol.Optional(CONF_RELEASE_SUMMARY): cv.string, + vol.Optional(CONF_RELEASE_URL): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_TITLE): cv.string, }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -99,10 +109,22 @@ def __init__( """Initialize the MQTT update.""" self._config = config self._attr_device_class = self._config.get(CONF_DEVICE_CLASS) + self._attr_release_summary = self._config.get(CONF_RELEASE_SUMMARY) + self._attr_release_url = self._config.get(CONF_RELEASE_URL) + self._attr_title = self._config.get(CONF_TITLE) + self._entity_picture: str | None = self._config.get(CONF_ENTITY_PICTURE) UpdateEntity.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend.""" + if self._entity_picture is not None: + return self._entity_picture + + return super().entity_picture + @staticmethod def config_schema() -> vol.Schema: """Return the config schema.""" @@ -138,15 +160,59 @@ def add_subscription( @callback @log_messages(self.hass, self.entity_id) - def handle_installed_version_received(msg: ReceiveMessage) -> None: - """Handle receiving installed version via MQTT.""" - installed_version = self._templates[CONF_VALUE_TEMPLATE](msg.payload) + def handle_state_message_received(msg: ReceiveMessage) -> None: + """Handle receiving state message via MQTT.""" + payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) + + if not payload or payload == PAYLOAD_EMPTY_JSON: + _LOGGER.debug( + "Ignoring empty payload '%s' after rendering for topic %s", + payload, + msg.topic, + ) + return + + json_payload = {} + try: + json_payload = json_loads(payload) + _LOGGER.debug( + "JSON payload detected after processing payload '%s' on topic %s", + json_payload, + msg.topic, + ) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.warning( + "No valid (JSON) payload detected after processing payload '%s' on topic %s", + json_payload, + msg.topic, + ) + json_payload["installed_version"] = payload + + if "installed_version" in json_payload: + self._attr_installed_version = json_payload["installed_version"] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if "latest_version" in json_payload: + self._attr_latest_version = json_payload["latest_version"] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if CONF_TITLE in json_payload and not self._attr_title: + self._attr_title = json_payload[CONF_TITLE] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary: + self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + + if CONF_RELEASE_URL in json_payload and not self._attr_release_url: + self._attr_release_url = json_payload[CONF_RELEASE_URL] + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - if isinstance(installed_version, str) and installed_version != "": - self._attr_installed_version = installed_version + if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture: + self._entity_picture = json_payload[CONF_ENTITY_PICTURE] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) - add_subscription(topics, CONF_STATE_TOPIC, handle_installed_version_received) + add_subscription(topics, CONF_STATE_TOPIC, handle_state_message_received) @callback @log_messages(self.hass, self.entity_id) diff --git a/tests/components/mqtt/test_update.py b/tests/components/mqtt/test_update.py index 9b008f093d08a0..e7d75ee7cc8073 100644 --- a/tests/components/mqtt/test_update.py +++ b/tests/components/mqtt/test_update.py @@ -6,7 +6,13 @@ from homeassistant.components import mqtt, update from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, + Platform, +) from homeassistant.setup import async_setup_component from .test_common import ( @@ -68,6 +74,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config): "state_topic": installed_version_topic, "latest_version_topic": latest_version_topic, "name": "Test Update", + "release_summary": "Test release summary", + "release_url": "https://example.com/release", + "title": "Test Update Title", + "entity_picture": "https://example.com/icon.png", } } }, @@ -84,6 +94,10 @@ async def test_run_update_setup(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "1.9.0" + assert state.attributes.get("release_summary") == "Test release summary" + assert state.attributes.get("release_url") == "https://example.com/release" + assert state.attributes.get("title") == "Test Update Title" + assert state.attributes.get("entity_picture") == "https://example.com/icon.png" async_fire_mqtt_message(hass, latest_version_topic, "2.0.0") @@ -126,6 +140,10 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_OFF assert state.attributes.get("installed_version") == "1.9.0" assert state.attributes.get("latest_version") == "1.9.0" + assert ( + state.attributes.get("entity_picture") + == "https://brands.home-assistant.io/_/mqtt/icon.png" + ) async_fire_mqtt_message(hass, latest_version_topic, '{"latest":"2.0.0"}') @@ -137,6 +155,120 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("latest_version") == "2.0.0" +async def test_empty_json_state_message(hass, mqtt_mock_entry_with_yaml_config): + """Test an empty JSON payload.""" + state_topic = "test/state-topic" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": state_topic, + "name": "Test Update", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, state_topic, "{}") + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_UNKNOWN + + +async def test_json_state_message(hass, mqtt_mock_entry_with_yaml_config): + """Test whether it fetches data from a JSON payload.""" + state_topic = "test/state-topic" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": state_topic, + "name": "Test Update", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message( + hass, + state_topic, + '{"installed_version":"1.9.0","latest_version":"1.9.0",' + '"title":"Test Update Title","release_url":"https://example.com/release",' + '"release_summary":"Test release summary"}', + ) + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "1.9.0" + assert state.attributes.get("release_summary") == "Test release summary" + assert state.attributes.get("release_url") == "https://example.com/release" + assert state.attributes.get("title") == "Test Update Title" + + async_fire_mqtt_message( + hass, + state_topic, + '{"installed_version":"1.9.0","latest_version":"2.0.0","title":"Test Update Title"}', + ) + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_ON + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "2.0.0" + + +async def test_json_state_message_with_template(hass, mqtt_mock_entry_with_yaml_config): + """Test whether it fetches data from a JSON payload with template.""" + state_topic = "test/state-topic" + await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + update.DOMAIN: { + "state_topic": state_topic, + "value_template": '{{ {"installed_version": value_json.installed, "latest_version": value_json.latest} | to_json }}', + "name": "Test Update", + } + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"1.9.0"}') + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_OFF + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "1.9.0" + + async_fire_mqtt_message(hass, state_topic, '{"installed":"1.9.0","latest":"2.0.0"}') + + await hass.async_block_till_done() + + state = hass.states.get("update.test_update") + assert state.state == STATE_ON + assert state.attributes.get("installed_version") == "1.9.0" + assert state.attributes.get("latest_version") == "2.0.0" + + async def test_run_install_service(hass, mqtt_mock_entry_with_yaml_config): """Test that install service works.""" installed_version_topic = "test/installed-version" From c2c57712d2427aa77449e54e79cae8fd712a63ab Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:51:20 +0100 Subject: [PATCH 142/394] Tuya configuration for `tuya_manufacturer` cluster (#81311) * Tuya configuration for tuya_manufacturer cluster * fix codespell * Add attributes initialization * Fix pylint complaints --- .../zha/core/channels/manufacturerspecific.py | 35 +++++++++++ .../components/zha/core/registries.py | 1 + homeassistant/components/zha/select.py | 59 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 5139854d66abbf..814e7700d01e04 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -59,6 +59,41 @@ class PhillipsRemote(ZigbeeChannel): REPORT_CONFIG = () +@registries.CHANNEL_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.TUYA_MANUFACTURER_CLUSTER) +class TuyaChannel(ZigbeeChannel): + """Channel for the Tuya manufacturer Zigbee cluster.""" + + REPORT_CONFIG = () + + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: + """Initialize TuyaChannel.""" + super().__init__(cluster, ch_pool) + + if self.cluster.endpoint.manufacturer in ( + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + ): + self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + "backlight_mode": True, + "power_on_state": True, + } + + @registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0) @registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0) class OppleRemote(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 2480cf1cd43a20..42f6bb55f5199a 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -33,6 +33,7 @@ SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 +TUYA_MANUFACTURER_CLUSTER = 0xEF00 VOC_LEVEL_CLUSTER = 0x042E REMOTE_DEVICE_TYPES = { diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 38f2f417643364..5ac0ec6d16408f 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -240,6 +240,27 @@ class TuyaPowerOnState(types.enum8): channel_names=CHANNEL_ON_OFF, models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"}, ) +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + }, +) class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_state"): """Representation of a ZHA power on state select entity.""" @@ -248,6 +269,44 @@ class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_stat _attr_name = "Power on state" +class MoesBacklightMode(types.enum8): + """MOES switch backlight mode enum.""" + + Off = 0x00 + LightWhenOn = 0x01 + LightWhenOff = 0x02 + Freeze = 0x03 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_7tdtqgwv", + "_TZE200_amp6tsvy", + "_TZE200_oisqyl4o", + "_TZE200_vhy3iakz", + "_TZ3000_uim07oem", + "_TZE200_wfxuhoea", + "_TZE200_tviaymwx", + "_TZE200_g1ib5ldv", + "_TZE200_wunufsil", + "_TZE200_7deq70b8", + "_TZE200_tz32mtza", + "_TZE200_2hf7x9n3", + "_TZE200_aqnazj70", + "_TZE200_1ozguk6x", + "_TZE200_k6jhsr0q", + "_TZE200_9mahtqtg", + }, +) +class MoesBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"): + """Moes devices have a different backlight mode select options.""" + + _select_attr = "backlight_mode" + _enum = MoesBacklightMode + _attr_name = "Backlight mode" + + class AqaraMotionSensitivities(types.enum8): """Aqara motion sensitivities.""" From 1cc85f77e30e7e87e17fb2e78229c71146ea6820 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 1 Nov 2022 10:10:30 +0100 Subject: [PATCH 143/394] Add task id attribute to fireservicerota sensor (#81323) --- homeassistant/components/fireservicerota/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fireservicerota/sensor.py b/homeassistant/components/fireservicerota/sensor.py index 36455da9fb7d0f..1484ff7f1543a1 100644 --- a/homeassistant/components/fireservicerota/sensor.py +++ b/homeassistant/components/fireservicerota/sensor.py @@ -79,6 +79,7 @@ def extra_state_attributes(self) -> dict[str, Any]: "type", "responder_mode", "can_respond_until", + "task_ids", ): if data.get(value): attr[value] = data[value] From f9493bc313d987302bb27664bdccab49aeb798bc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 1 Nov 2022 02:08:36 -0700 Subject: [PATCH 144/394] Bump gcal_sync to 2.2.2 and fix recurring event bug (#81339) * Bump gcal_sync to 2.2.2 and fix recurring event bug * Bump to 2.2.2 --- 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 ce95e3112ee91b..9a184bdd636a2e 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==2.2.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==2.2.2", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 662bc2b507562b..e233d757e8afcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.0 +gcal-sync==2.2.2 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be8b850e6b4651..6866672df14e45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.0 +gcal-sync==2.2.2 # homeassistant.components.geocaching geocachingapi==0.2.1 From 473490aee773c0ba1c3089e614bf6747bd945a08 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 1 Nov 2022 12:53:44 +0200 Subject: [PATCH 145/394] Bump aioshelly to 4.1.2 (#81342) --- homeassistant/components/shelly/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 07499ce1e9d5f5..70970e73e307ae 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==4.1.1"], + "requirements": ["aioshelly==4.1.2"], "dependencies": ["http"], "zeroconf": [ { diff --git a/requirements_all.txt b/requirements_all.txt index e233d757e8afcc..b48643758e5bfc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -255,7 +255,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.1 +aioshelly==4.1.2 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6866672df14e45..bf716af6b6ec2b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==4.1.1 +aioshelly==4.1.2 # homeassistant.components.skybell aioskybell==22.7.0 From c4bb225060085fb0e2732647b13ab4bffb9e523e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 10:17:01 +0100 Subject: [PATCH 146/394] Fix power/energy mixup in Youless (#81345) --- homeassistant/components/youless/sensor.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index 19e9c635dce017..53ffb22393917a 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -39,13 +39,13 @@ async def async_setup_entry( async_add_entities( [ GasSensor(coordinator, device), - PowerMeterSensor( + EnergyMeterSensor( coordinator, device, "low", SensorStateClass.TOTAL_INCREASING ), - PowerMeterSensor( + EnergyMeterSensor( coordinator, device, "high", SensorStateClass.TOTAL_INCREASING ), - PowerMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL), + EnergyMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL), CurrentPowerSensor(coordinator, device), DeliveryMeterSensor(coordinator, device, "low"), DeliveryMeterSensor(coordinator, device, "high"), @@ -68,10 +68,6 @@ def __init__( ) -> None: """Create the sensor.""" super().__init__(coordinator) - self._device = device - self._device_group = device_group - self._sensor_id = sensor_id - self._attr_unique_id = f"{DOMAIN}_{device}_{sensor_id}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{device}_{device_group}")}, @@ -149,10 +145,10 @@ def __init__( ) -> None: """Instantiate a delivery meter sensor.""" super().__init__( - coordinator, device, "delivery", "Power delivery", f"delivery_{dev_type}" + coordinator, device, "delivery", "Energy delivery", f"delivery_{dev_type}" ) self._type = dev_type - self._attr_name = f"Power delivery {dev_type}" + self._attr_name = f"Energy delivery {dev_type}" @property def get_sensor(self) -> YoulessSensor | None: @@ -163,7 +159,7 @@ def get_sensor(self) -> YoulessSensor | None: return getattr(self.coordinator.data.delivery_meter, f"_{self._type}", None) -class PowerMeterSensor(YoulessBaseSensor): +class EnergyMeterSensor(YoulessBaseSensor): """The Youless low meter value sensor.""" _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR @@ -177,13 +173,13 @@ def __init__( dev_type: str, state_class: SensorStateClass, ) -> None: - """Instantiate a power meter sensor.""" + """Instantiate a energy meter sensor.""" super().__init__( - coordinator, device, "power", "Power usage", f"power_{dev_type}" + coordinator, device, "power", "Energy usage", f"power_{dev_type}" ) self._device = device self._type = dev_type - self._attr_name = f"Power {dev_type}" + self._attr_name = f"Energy {dev_type}" self._attr_state_class = state_class @property From a2d432dfd65e7404de4e868c1b75f0c608cbbfa2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 1 Nov 2022 16:25:01 +0100 Subject: [PATCH 147/394] Revert "Do not write state if payload is `''`" for MQTT sensor (#81347) * Revert "Do not write state if payload is ''" This reverts commit 869c11884e2b06d5f5cb5a8a4f78247a6972149e. * Add test --- homeassistant/components/mqtt/sensor.py | 4 ++-- tests/components/mqtt/test_sensor.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index d95d669e72f6cf..52ba1a7e3c28e1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -271,8 +271,8 @@ def _update_state(msg): ) elif self.device_class == SensorDeviceClass.DATE: payload = payload.date() - if payload != "": - self._state = payload + + self._state = payload def _update_last_reset(msg): payload = self._last_reset_template(msg.payload) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 6cfaa9678bba3d..1884d04efc3073 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -313,6 +313,12 @@ async def test_setting_sensor_value_via_mqtt_json_message( assert state.state == "100" + # Make sure the state is written when a sensor value is reset to '' + async_fire_mqtt_message(hass, "test-topic", '{ "val": "" }') + state = hass.states.get("sensor.test") + + assert state.state == "" + async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state( hass, mqtt_mock_entry_with_yaml_config From f265c160d17ff435796f35c769570beac8b13969 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Nov 2022 15:57:48 +0100 Subject: [PATCH 148/394] Lower log level for non-JSON payload in MQTT update (#81348) Change log level --- homeassistant/components/mqtt/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 986ad013520205..5536d16d1c77eb 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -181,9 +181,9 @@ def handle_state_message_received(msg: ReceiveMessage) -> None: msg.topic, ) except JSON_DECODE_EXCEPTIONS: - _LOGGER.warning( + _LOGGER.debug( "No valid (JSON) payload detected after processing payload '%s' on topic %s", - json_payload, + payload, msg.topic, ) json_payload["installed_version"] = payload From d0ddbb5f5807b798434d5441a4dc693583c4424e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 1 Nov 2022 09:06:55 -0400 Subject: [PATCH 149/394] Fix individual LED range for ZHA device action (#81351) The inovelli individual LED effect device action can address 7 LEDs. I had set the range 1-7 but it should be 0-6. --- homeassistant/components/zha/device_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 1cb988b1c1513f..3e2a3591c804cf 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -93,7 +93,7 @@ ), INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema( { - vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)), + vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), vol.Required("effect_type"): vol.In( InovelliConfigEntityChannel.LEDEffectType.__members__.keys() ), From 9dff7ab6b96b8c5f78441e7e6865ba57d421ceff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 12:07:42 -0500 Subject: [PATCH 150/394] Adjust time to remove stale connectable devices from the esphome ble to closer match bluez (#81356) --- .../components/esphome/bluetooth/scanner.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 7c8064d5583d04..4fbaf7cabb6e0e 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -6,6 +6,7 @@ from datetime import timedelta import re import time +from typing import Final from aioesphomeapi import BluetoothLEAdvertisement from bleak.backends.device import BLEDevice @@ -23,6 +24,15 @@ TWO_CHAR = re.compile("..") +# The maximum time between advertisements for a device to be considered +# stale when the advertisement tracker can determine the interval for +# connectable devices. +# +# BlueZ uses 180 seconds by default but we give it a bit more time +# to account for the esp32's bluetooth stack being a bit slower +# than BlueZ's. +CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 195 + class ESPHomeScanner(BaseHaScanner): """Scanner for esphome.""" @@ -45,8 +55,12 @@ def __init__( self._connector = connector self._connectable = connectable self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} + self._fallback_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS if connectable: self._details["connector"] = connector + self._fallback_seconds = ( + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) @callback def async_setup(self) -> CALLBACK_TYPE: @@ -61,7 +75,7 @@ def _async_expire_devices(self, _datetime: datetime.datetime) -> None: expired = [ address for address, timestamp in self._discovered_device_timestamps.items() - if now - timestamp > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + if now - timestamp > self._fallback_seconds ] for address in expired: del self._discovered_device_advertisement_datas[address] From 8c63a9ce5e29de71e3dd3db5816c8a48e8bc4064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 12:07:03 -0500 Subject: [PATCH 151/394] Immediately prefer advertisements from alternate sources when a scanner goes away (#81357) --- homeassistant/components/bluetooth/manager.py | 5 + tests/components/bluetooth/test_manager.py | 92 +++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index c3a0e0998f1919..d29023acef78ea 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -127,6 +127,7 @@ def __init__( self._non_connectable_scanners: list[BaseHaScanner] = [] self._connectable_scanners: list[BaseHaScanner] = [] self._adapters: dict[str, AdapterDetails] = {} + self._sources: set[str] = set() @property def supports_passive_scan(self) -> bool: @@ -379,6 +380,7 @@ def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: if ( (old_service_info := all_history.get(address)) and source != old_service_info.source + and old_service_info.source in self._sources and self._prefer_previous_adv_from_different_source( old_service_info, service_info ) @@ -398,6 +400,7 @@ def scanner_adv_received(self, service_info: BluetoothServiceInfoBleak) -> None: # the old connectable advertisement or ( source != old_connectable_service_info.source + and old_connectable_service_info.source in self._sources and self._prefer_previous_adv_from_different_source( old_connectable_service_info, service_info ) @@ -597,8 +600,10 @@ def async_register_scanner( def _unregister_scanner() -> None: self._advertisement_tracker.async_remove_source(scanner.source) scanners.remove(scanner) + self._sources.remove(scanner.source) scanners.append(scanner) + self._sources.add(scanner.source) return _unregister_scanner @hass_callback diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index c6a65046ef905c..0375f68309f717 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -5,11 +5,14 @@ from bleak.backends.scanner import BLEDevice from bluetooth_adapters import AdvertisementHistory +import pytest from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import models from homeassistant.components.bluetooth.manager import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import ( @@ -20,8 +23,28 @@ ) +@pytest.fixture +def register_hci0_scanner(hass: HomeAssistant) -> None: + """Register an hci0 scanner.""" + cancel = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci0"), True + ) + yield + cancel() + + +@pytest.fixture +def register_hci1_scanner(hass: HomeAssistant) -> None: + """Register an hci1 scanner.""" + cancel = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci1"), True + ) + yield + cancel() + + async def test_advertisements_do_not_switch_adapters_for_no_reason( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test we only switch adapters when needed.""" @@ -68,7 +91,9 @@ async def test_advertisements_do_not_switch_adapters_for_no_reason( ) -async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): +async def test_switching_adapters_based_on_rssi( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on rssi.""" address = "44:44:33:11:23:45" @@ -122,7 +147,9 @@ async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): ) -async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): +async def test_switching_adapters_based_on_zero_rssi( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on zero rssi.""" address = "44:44:33:11:23:45" @@ -176,7 +203,9 @@ async def test_switching_adapters_based_on_zero_rssi(hass, enable_bluetooth): ) -async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): +async def test_switching_adapters_based_on_stale( + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner +): """Test switching adapters based on the previous advertisement being stale.""" address = "44:44:33:11:23:41" @@ -256,7 +285,7 @@ async def test_restore_history_from_dbus(hass, one_adapter): async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test switching adapters based on rssi from connectable to non connectable.""" @@ -339,7 +368,7 @@ async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_connectable( - hass, enable_bluetooth + hass, enable_bluetooth, register_hci0_scanner, register_hci1_scanner ): """Test we can still get a connectable BLEDevice when the best path is non-connectable. @@ -384,3 +413,54 @@ async def test_connectable_advertisement_can_be_retrieved_with_best_path_is_non_ bluetooth.async_ble_device_from_address(hass, address, True) is switchbot_device_poor_signal ) + + +async def test_switching_adapters_when_one_goes_away( + hass, enable_bluetooth, register_hci0_scanner +): + """Test switching adapters when one goes away.""" + cancel_hci2 = bluetooth.async_register_scanner( + hass, models.BaseHaScanner(hass, "hci2"), True + ) + + address = "44:44:33:11:23:45" + + switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal") + switchbot_adv_good_signal = generate_advertisement_data( + local_name="wohand_good_signal", service_uuids=[], rssi=-60 + ) + inject_advertisement_with_source( + hass, switchbot_device_good_signal, switchbot_adv_good_signal, "hci2" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal") + switchbot_adv_poor_signal = generate_advertisement_data( + local_name="wohand_poor_signal", service_uuids=[], rssi=-100 + ) + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # We want to prefer the good signal when we have options + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + cancel_hci2() + + inject_advertisement_with_source( + hass, switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + # Now that hci2 is gone, we should prefer the poor signal + # since no poor signal is better than no signal + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal + ) From 1efec8323abf6ef1e3896c65ebac76cbd24a3613 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 11:44:58 -0500 Subject: [PATCH 152/394] Bump aiohomekit to 2.2.11 (#81358) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 93aae62daab91e..6533d7f29beaa7 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.10"], + "requirements": ["aiohomekit==2.2.11"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index b48643758e5bfc..b751dc428ad182 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.10 +aiohomekit==2.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf716af6b6ec2b..bd19d6861184d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.10 +aiohomekit==2.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http From e8f93d9c7f943482bb10692fa255093c36747faa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 Nov 2022 13:09:48 -0400 Subject: [PATCH 153/394] Bumped version to 2022.11.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f547e536ae00a2..195b52c4debf25 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 5f2aa4d4311f09..dd549dfeb0140a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b5" +version = "2022.11.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b4637fae37661dac6af5685912dac34a4ae4dc7c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:57:53 -0400 Subject: [PATCH 154/394] Bump zigpy-zigate to 0.10.3 (#81363) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 79980d763e7f15..e40a54c11bce99 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -11,7 +11,7 @@ "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", - "zigpy-zigate==0.10.2", + "zigpy-zigate==0.10.3", "zigpy-znp==0.9.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index aea2dbd2563705..5cd546685f0480 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2625,7 +2625,7 @@ zigpy-deconz==0.19.0 zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.2 +zigpy-zigate==0.10.3 # homeassistant.components.zha zigpy-znp==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80ef0d3228e972..593e262b8bdc20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1820,7 +1820,7 @@ zigpy-deconz==0.19.0 zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.2 +zigpy-zigate==0.10.3 # homeassistant.components.zha zigpy-znp==0.9.1 From 5c99e2e5d3ae40c7107ab0e7576f330f8721ee88 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 19:11:50 +0100 Subject: [PATCH 155/394] Improve error logging of WebSocket API (#81360) --- .../components/websocket_api/connection.py | 12 ++- .../websocket_api/test_connection.py | 92 +++++++++++++++---- 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index c344e1c6a9fd4e..ab4dda845db9f7 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User +from homeassistant.components.http import current_request from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized @@ -137,6 +138,13 @@ def async_handle_exception(self, msg: dict[str, Any], err: Exception) -> None: err_message = "Unknown error" log_handler = self.logger.exception - log_handler("Error handling message: %s (%s)", err_message, code) - self.send_message(messages.error_message(msg["id"], code, err_message)) + + if code: + err_message += f" ({code})" + if request := current_request.get(): + err_message += f" from {request.remote}" + if user_agent := request.headers.get("user-agent"): + err_message += f" ({user_agent})" + + log_handler("Error handling message: %s", err_message) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index fd9af99c1a48d3..8f2cd43fdb8b76 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -1,8 +1,11 @@ """Test WebSocket Connection class.""" import asyncio import logging -from unittest.mock import Mock +from typing import Any +from unittest.mock import AsyncMock, Mock, patch +from aiohttp.test_utils import make_mocked_request +import pytest import voluptuous as vol from homeassistant import exceptions @@ -11,37 +14,86 @@ from tests.common import MockUser -async def test_exception_handling(): - """Test handling of exceptions.""" - send_messages = [] - user = MockUser() - refresh_token = Mock() - conn = websocket_api.ActiveConnection( - logging.getLogger(__name__), None, send_messages.append, user, refresh_token - ) - - for (exc, code, err) in ( - (exceptions.Unauthorized(), websocket_api.ERR_UNAUTHORIZED, "Unauthorized"), +@pytest.mark.parametrize( + "exc,code,err,log", + [ + ( + exceptions.Unauthorized(), + websocket_api.ERR_UNAUTHORIZED, + "Unauthorized", + "Error handling message: Unauthorized (unauthorized) from 127.0.0.42 (Browser)", + ), ( vol.Invalid("Invalid something"), websocket_api.ERR_INVALID_FORMAT, "Invalid something. Got {'id': 5}", + "Error handling message: Invalid something. Got {'id': 5} (invalid_format) from 127.0.0.42 (Browser)", + ), + ( + asyncio.TimeoutError(), + websocket_api.ERR_TIMEOUT, + "Timeout", + "Error handling message: Timeout (timeout) from 127.0.0.42 (Browser)", ), - (asyncio.TimeoutError(), websocket_api.ERR_TIMEOUT, "Timeout"), ( exceptions.HomeAssistantError("Failed to do X"), websocket_api.ERR_UNKNOWN_ERROR, "Failed to do X", + "Error handling message: Failed to do X (unknown_error) from 127.0.0.42 (Browser)", + ), + ( + ValueError("Really bad"), + websocket_api.ERR_UNKNOWN_ERROR, + "Unknown error", + "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)", ), - (ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"), ( - exceptions.HomeAssistantError(), + exceptions.HomeAssistantError, websocket_api.ERR_UNKNOWN_ERROR, "Unknown error", + "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)", ), - ): - send_messages.clear() + ], +) +async def test_exception_handling( + caplog: pytest.LogCaptureFixture, + exc: Exception, + code: str, + err: str, + log: str, +): + """Test handling of exceptions.""" + send_messages = [] + user = MockUser() + refresh_token = Mock() + current_request = AsyncMock() + + def get_extra_info(key: str) -> Any: + if key == "sslcontext": + return True + + if key == "peername": + return ("127.0.0.42", 8123) + + mocked_transport = Mock() + mocked_transport.get_extra_info = get_extra_info + mocked_request = make_mocked_request( + "GET", + "/api/websocket", + headers={"Host": "example.com", "User-Agent": "Browser"}, + transport=mocked_transport, + ) + + with patch( + "homeassistant.components.websocket_api.connection.current_request", + ) as current_request: + current_request.get.return_value = mocked_request + conn = websocket_api.ActiveConnection( + logging.getLogger(__name__), None, send_messages.append, user, refresh_token + ) + conn.async_handle_exception({"id": 5}, exc) - assert len(send_messages) == 1 - assert send_messages[0]["error"]["code"] == code - assert send_messages[0]["error"]["message"] == err + assert len(send_messages) == 1 + assert send_messages[0]["error"]["code"] == code + assert send_messages[0]["error"]["message"] == err + assert log in caplog.text From 914ccdbc4f931603aebc009983315ed15e695a59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 13:21:20 -0500 Subject: [PATCH 156/394] Fix unload race in unifiprotect tests (#81361) --- .../unifiprotect/test_config_flow.py | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index d0fb0dba9f27b7..dc91b0f6ddec3b 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -254,23 +254,28 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - await hass.async_block_till_done() assert mock_config.state == config_entries.ConfigEntryState.LOADED - result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == FlowResultType.FORM - assert not result["errors"] - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(mock_config.entry_id) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + assert result["step_id"] == "init" - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - {CONF_DISABLE_RTSP: True, CONF_ALL_UPDATES: True, CONF_OVERRIDE_CHOST: True}, - ) + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + { + CONF_DISABLE_RTSP: True, + CONF_ALL_UPDATES: True, + CONF_OVERRIDE_CHOST: True, + }, + ) - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["data"] == { - "all_updates": True, - "disable_rtsp": True, - "override_connection_host": True, - "max_media": 1000, - } + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "all_updates": True, + "disable_rtsp": True, + "override_connection_host": True, + "max_media": 1000, + } + await hass.config_entries.async_unload(mock_config.entry_id) @pytest.mark.parametrize( From 54df052699b02ba32e80610cc6a10a3f20506c70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 15:49:05 -0500 Subject: [PATCH 157/394] Bump bleak-retry-connector to 2.8.2 (#81370) * Bump bleak-retry-connector to 2.8.2 Tweaks for the esp32 proxies now that we have better error reporting. This change improves the retry cases a bit with the new https://github.com/esphome/esphome/pull/3971 * empty --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index bca2f7f9a8d1c7..9c2438b8b189ee 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.1", + "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.60.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a2f4b5a7f84ab..db85880ffbd3d2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 5cd546685f0480..494967f62e149b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -416,7 +416,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 593e262b8bdc20..3afe6ee54b3c31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -340,7 +340,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 # homeassistant.components.bluetooth bleak==0.19.1 From 329466d131e57a2572f791b302601bfb62aa75c0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 1 Nov 2022 22:58:07 +0100 Subject: [PATCH 158/394] Enable strict typing for NextDNS (#81378) --- .strict-typing | 1 + mypy.ini | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.strict-typing b/.strict-typing index dce6fcc6745103..330424faec7594 100644 --- a/.strict-typing +++ b/.strict-typing @@ -191,6 +191,7 @@ homeassistant.components.neato.* homeassistant.components.nest.* homeassistant.components.netatmo.* homeassistant.components.network.* +homeassistant.components.nextdns.* homeassistant.components.nfandroidtv.* homeassistant.components.nissan_leaf.* homeassistant.components.no_ip.* diff --git a/mypy.ini b/mypy.ini index c546377ae4fcab..3f0e917f167977 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1662,6 +1662,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.nextdns.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.nfandroidtv.*] check_untyped_defs = true disallow_incomplete_defs = true From 054a271bd2e84001ffac7e4e1e9b5ae7bc6e2f7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 17:00:04 -0500 Subject: [PATCH 159/394] Bump aiohomekit to 2.2.12 (#81372) * Bump aiohomekit to 2.2.12 Fixes a missing lock which was noticable on the esp32s since they disconnect right away when you ask for gatt notify. https://github.com/Jc2k/aiohomekit/compare/2.2.11...2.2.12 * empty --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 6533d7f29beaa7..09f2a15871f822 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.11"], + "requirements": ["aiohomekit==2.2.12"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 494967f62e149b..50c5079438404a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.11 +aiohomekit==2.2.12 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3afe6ee54b3c31..e0230fa1d27418 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.11 +aiohomekit==2.2.12 # homeassistant.components.emulated_hue # homeassistant.components.http From 697a81c4a361a5407bb51f7828b07315492f247e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 2 Nov 2022 00:30:01 +0000 Subject: [PATCH 160/394] [ci skip] Translation update --- .../components/aranet/translations/ca.json | 5 +++- .../components/aranet/translations/it.json | 25 +++++++++++++++++++ .../components/aranet/translations/pl.json | 25 +++++++++++++++++++ .../aranet/translations/zh-Hant.json | 25 +++++++++++++++++++ .../fireservicerota/translations/bg.json | 5 ++++ .../fireservicerota/translations/de.json | 6 +++++ .../fireservicerota/translations/et.json | 6 +++++ .../fireservicerota/translations/it.json | 6 +++++ .../fireservicerota/translations/no.json | 6 +++++ .../fireservicerota/translations/pl.json | 6 +++++ .../fireservicerota/translations/ru.json | 6 +++++ .../fireservicerota/translations/zh-Hant.json | 6 +++++ .../google_travel_time/translations/it.json | 1 + .../google_travel_time/translations/pl.json | 1 + .../translations/zh-Hant.json | 1 + .../components/hassio/translations/bg.json | 5 ++++ .../components/hassio/translations/de.json | 10 ++++++++ .../components/hassio/translations/it.json | 10 ++++++++ .../components/hassio/translations/no.json | 10 ++++++++ .../components/hassio/translations/pl.json | 10 ++++++++ .../hassio/translations/zh-Hant.json | 10 ++++++++ .../here_travel_time/translations/pl.json | 2 +- .../components/motioneye/translations/bg.json | 5 +++- .../ovo_energy/translations/ca.json | 1 + .../ovo_energy/translations/it.json | 1 + .../ovo_energy/translations/pl.json | 1 + .../components/scrape/translations/ca.json | 5 ++++ .../components/scrape/translations/it.json | 6 +++++ .../components/scrape/translations/pl.json | 6 +++++ .../scrape/translations/zh-Hant.json | 6 +++++ .../components/subaru/translations/bg.json | 3 +++ .../transmission/translations/it.json | 13 ++++++++++ .../transmission/translations/pl.json | 13 ++++++++++ .../transmission/translations/zh-Hant.json | 13 ++++++++++ 34 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/aranet/translations/it.json create mode 100644 homeassistant/components/aranet/translations/pl.json create mode 100644 homeassistant/components/aranet/translations/zh-Hant.json diff --git a/homeassistant/components/aranet/translations/ca.json b/homeassistant/components/aranet/translations/ca.json index 1bbc15cc1b71a9..ceaa1750762dc7 100644 --- a/homeassistant/components/aranet/translations/ca.json +++ b/homeassistant/components/aranet/translations/ca.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "integrations_diabled": "Aquest dispositiu no t\u00e9 les integracions activades. Activa les integracions de llar intel\u00b7ligent a trav\u00e9s de l'aplicaci\u00f3 i torna-ho a provar.", + "no_devices_found": "No s'han trobat dispositius Aranet no configurats.", + "outdated_version": "El dispositiu est\u00e0 utilitzant programari obsolet. Actualitza'l com a m\u00ednim a la versi\u00f3 1.2.0 i torna-ho a provar." }, "error": { "unknown": "Error inesperat" diff --git a/homeassistant/components/aranet/translations/it.json b/homeassistant/components/aranet/translations/it.json new file mode 100644 index 00000000000000..372e6266b5b252 --- /dev/null +++ b/homeassistant/components/aranet/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "integrations_diabled": "Questo dispositivo non ha integrazioni abilitate. Si prega di abilitare le integrazioni di smart home utilizzando l'app e riprovare.", + "no_devices_found": "Non sono stati trovati dispositivi Aranet non configurati.", + "outdated_version": "Questo dispositivo utilizza un firmware obsoleto. Aggiornalo almeno alla v1.2.0 e riprova." + }, + "error": { + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/pl.json b/homeassistant/components/aranet/translations/pl.json new file mode 100644 index 00000000000000..3737a66471ea46 --- /dev/null +++ b/homeassistant/components/aranet/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "integrations_diabled": "To urz\u0105dzenie nie ma w\u0142\u0105czonej integracji. W\u0142\u0105cz integracj\u0119 z inteligentnym domem za pomoc\u0105 aplikacji i spr\u00f3buj ponownie.", + "no_devices_found": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144 Aranet.", + "outdated_version": "To urz\u0105dzenie korzysta z przestarza\u0142ego oprogramowania. Zaktualizuj go do wersji co najmniej 1.2.0 i spr\u00f3buj ponownie." + }, + "error": { + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/zh-Hant.json b/homeassistant/components/aranet/translations/zh-Hant.json new file mode 100644 index 00000000000000..586c871e04300b --- /dev/null +++ b/homeassistant/components/aranet/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "integrations_diabled": "\u88dd\u7f6e\u4e26\u672a\u555f\u7528\u4efb\u4f55\u6574\u5408\uff0c\u8acb\u5148\u4f7f\u7528 App \u555f\u7528\u667a\u80fd\u5bb6\u5ead\u6574\u5408\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "no_devices_found": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a Aranet \u88dd\u7f6e\u3002", + "outdated_version": "\u88dd\u7f6e\u4f7f\u7528\u4e86\u904e\u820a\u7684\u97cc\u9ad4\uff0c\u8acb\u66f4\u65b0\u81f3 v1.2.0 \u7248\u4ee5\u4e0a\u4e26\u518d\u8a66\u4e00\u6b21\u3002" + }, + "error": { + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/bg.json b/homeassistant/components/fireservicerota/translations/bg.json index 22cc783d4e91cb..422988d551b616 100644 --- a/homeassistant/components/fireservicerota/translations/bg.json +++ b/homeassistant/components/fireservicerota/translations/bg.json @@ -16,6 +16,11 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430" } }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/fireservicerota/translations/de.json b/homeassistant/components/fireservicerota/translations/de.json index c8c18c4c3723f3..5be147663ff936 100644 --- a/homeassistant/components/fireservicerota/translations/de.json +++ b/homeassistant/components/fireservicerota/translations/de.json @@ -17,6 +17,12 @@ }, "description": "Authentifizierungs-Tokens sind ung\u00fcltig, melde dich an, um sie neu zu erstellen." }, + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Die Authentifizierungs-Tokens wurden ung\u00fcltig, melde dich an, um sie neu zu erstellen." + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/fireservicerota/translations/et.json b/homeassistant/components/fireservicerota/translations/et.json index dedd74e8701288..c949db5fd9cc0e 100644 --- a/homeassistant/components/fireservicerota/translations/et.json +++ b/homeassistant/components/fireservicerota/translations/et.json @@ -17,6 +17,12 @@ }, "description": "Tuvastusstring aegus, taasloomiseks logi sisse." }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Autentimise m\u00e4rgised muutusid kehtetuks, logi sisse, et neid uuesti luua." + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/fireservicerota/translations/it.json b/homeassistant/components/fireservicerota/translations/it.json index 8a437e45900da4..1dbd077521e0b6 100644 --- a/homeassistant/components/fireservicerota/translations/it.json +++ b/homeassistant/components/fireservicerota/translations/it.json @@ -17,6 +17,12 @@ }, "description": "I token di autenticazione non sono validi, esegui l'accesso per ricrearli." }, + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "I token di autenticazione non sono pi\u00f9 validi, accedi per ricrearli." + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json index 03ecc365e743a1..9b228f15ccddda 100644 --- a/homeassistant/components/fireservicerota/translations/no.json +++ b/homeassistant/components/fireservicerota/translations/no.json @@ -17,6 +17,12 @@ }, "description": "Autentiseringstokener ble ugyldige, logg inn for \u00e5 gjenskape dem." }, + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Autentiseringstokener ble ugyldige, logg p\u00e5 for \u00e5 gjenskape dem." + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/fireservicerota/translations/pl.json b/homeassistant/components/fireservicerota/translations/pl.json index 2e5e480fcc1450..b0c45bea46bd1e 100644 --- a/homeassistant/components/fireservicerota/translations/pl.json +++ b/homeassistant/components/fireservicerota/translations/pl.json @@ -17,6 +17,12 @@ }, "description": "Tokeny uwierzytelniaj\u0105ce straci\u0142y wa\u017cno\u015b\u0107. Zaloguj si\u0119, aby je odtworzy\u0107." }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Tokeny uwierzytelniaj\u0105ce straci\u0142y wa\u017cno\u015b\u0107. Zaloguj si\u0119, aby je odtworzy\u0107." + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/fireservicerota/translations/ru.json b/homeassistant/components/fireservicerota/translations/ru.json index 046a65081ec61b..b6ae8c4280e3f6 100644 --- a/homeassistant/components/fireservicerota/translations/ru.json +++ b/homeassistant/components/fireservicerota/translations/ru.json @@ -17,6 +17,12 @@ }, "description": "\u0422\u043e\u043a\u0435\u043d\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b, \u0432\u043e\u0439\u0434\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0445 \u0437\u0430\u043d\u043e\u0432\u043e." }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u043e\u043a\u0435\u043d\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b, \u0432\u043e\u0439\u0434\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u0445 \u0437\u0430\u043d\u043e\u0432\u043e." + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/fireservicerota/translations/zh-Hant.json b/homeassistant/components/fireservicerota/translations/zh-Hant.json index 8e5f4d9f20db82..af84ae5eab3eca 100644 --- a/homeassistant/components/fireservicerota/translations/zh-Hant.json +++ b/homeassistant/components/fireservicerota/translations/zh-Hant.json @@ -17,6 +17,12 @@ }, "description": "\u8a8d\u8b49\u6b0a\u6756\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002" }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u8a8d\u8b49\u6b0a\u6756\u5df2\u7d93\u5931\u6548\uff0c\u8acb\u767b\u5165\u91cd\u65b0\u65b0\u589e\u3002" + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/google_travel_time/translations/it.json b/homeassistant/components/google_travel_time/translations/it.json index 16dc9f85f665fe..8648bf33d6d082 100644 --- a/homeassistant/components/google_travel_time/translations/it.json +++ b/homeassistant/components/google_travel_time/translations/it.json @@ -28,6 +28,7 @@ "mode": "Modalit\u00e0 di viaggio", "time": "Ora", "time_type": "Tipo di ora", + "traffic_mode": "Modalit\u00e0 traffico", "transit_mode": "Modalit\u00e0 di transito", "transit_routing_preference": "Preferenza percorso di transito", "units": "Unit\u00e0" diff --git a/homeassistant/components/google_travel_time/translations/pl.json b/homeassistant/components/google_travel_time/translations/pl.json index 0890a4fd350d28..1ced0b912e675b 100644 --- a/homeassistant/components/google_travel_time/translations/pl.json +++ b/homeassistant/components/google_travel_time/translations/pl.json @@ -28,6 +28,7 @@ "mode": "Tryb podr\u00f3\u017cy", "time": "Czas", "time_type": "Typ czasu", + "traffic_mode": "Tryb nat\u0119\u017cenia ruchu", "transit_mode": "Tryb tranzytu", "transit_routing_preference": "Preferencje trasy tranzytowej", "units": "Jednostki" diff --git a/homeassistant/components/google_travel_time/translations/zh-Hant.json b/homeassistant/components/google_travel_time/translations/zh-Hant.json index 29bf50f3376538..a964f0a09f19b1 100644 --- a/homeassistant/components/google_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/google_travel_time/translations/zh-Hant.json @@ -28,6 +28,7 @@ "mode": "\u65c5\u884c\u6a21\u5f0f", "time": "\u6642\u9593", "time_type": "\u6642\u9593\u985e\u5225", + "traffic_mode": "\u4ea4\u901a\u6a21\u5f0f", "transit_mode": "\u79fb\u52d5\u6a21\u5f0f", "transit_routing_preference": "\u504f\u597d\u79fb\u52d5\u8def\u7dda", "units": "\u55ae\u4f4d" diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index 68fcf5f343e8f0..451dd51d1257dd 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -1,4 +1,9 @@ { + "issues": { + "unsupported": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + } + }, "system_health": { "info": { "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0430\u0433\u0435\u043d\u0442\u0430", diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index f25ae73b423e78..54940b93448044 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "Das System ist derzeit aufgrund von \u201e{reason}\u201c fehlerhaft. Verwende den Link, um mehr dar\u00fcber zu erfahren, was falsch ist und wie du es beheben kannst.", + "title": "Fehlerhaftes System - {reason}" + }, + "unsupported": { + "description": "Das System wird aufgrund von \u201e{reason}\u201c nicht unterst\u00fctzt. Verwende den Link, um mehr dar\u00fcber zu erfahren, was dies bedeutet und wie du zu einem unterst\u00fctzten System zur\u00fcckkehren kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "Agent-Version", diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 3dc55d0f52512e..c305b9fce34759 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "Il sistema non \u00e8 attualmente integro a causa di '{reason}'. Usa il collegamento per saperne di pi\u00f9 sul problema e su come risolverlo.", + "title": "Sistema non pi\u00f9 integro - {reason}" + }, + "unsupported": { + "description": "Il sistema non \u00e8 supportato a causa di '{reason}'. Utilizzare il collegamento per ulteriori informazioni sul significato e su come tornare a un sistema supportato.", + "title": "Sistema non supportato - {reason}" + } + }, "system_health": { "info": { "agent_version": "Versione agente", diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index 1fa10a98921c91..8dd5d471860402 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "Systemet er for \u00f8yeblikket usunt p\u00e5 grunn av '{reason}'. Bruk linken for \u00e5 l\u00e6re mer om hva som er galt og hvordan du kan fikse det.", + "title": "Usunt system \u2013 {reason}" + }, + "unsupported": { + "description": "Systemet st\u00f8ttes ikke p\u00e5 grunn av '{reason}'. Bruk lenken for \u00e5 l\u00e6re mer om hva dette betyr og hvordan du g\u00e5r tilbake til et st\u00f8ttet system.", + "title": "Systemet st\u00f8ttes ikke \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "Agentversjon", diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json index 8850b7066fd4d9..0fdbfa2ab68beb 100644 --- a/homeassistant/components/hassio/translations/pl.json +++ b/homeassistant/components/hassio/translations/pl.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "System jest obecnie \"niezdrowy\" z powodu \u201e{reason}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej o tym, co jest nie tak i jak to naprawi\u0107.", + "title": "Niezdrowy system \u2013 {reason}" + }, + "unsupported": { + "description": "System nie jest obs\u0142ugiwany z powodu \u201e{pow\u00f3d}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej o tym, co to oznacza i jak wr\u00f3ci\u0107 do obs\u0142ugiwanego systemu.", + "title": "Nieobs\u0142ugiwany system \u2013 {reason}" + } + }, "system_health": { "info": { "agent_version": "Wersja agenta", diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json index 5a503e54937f0a..8194ea37e6d9be 100644 --- a/homeassistant/components/hassio/translations/zh-Hant.json +++ b/homeassistant/components/hassio/translations/zh-Hant.json @@ -1,4 +1,14 @@ { + "issues": { + "unhealthy": { + "description": "System is currently unhealthy \u7531\u65bc '{reason}' \u7de3\u6545\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u54ea\u88e1\u51fa\u4e86\u554f\u984c\u3001\u53ca\u5982\u4f55\u9032\u884c\u4fee\u6b63\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - {reason}" + }, + "unsupported": { + "description": "System is unsupported \u7531\u65bc '{reason}' \u7de3\u6545\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u76f8\u95dc\u8aaa\u660e\u3001\u53ca\u5982\u4f55\u56de\u5fa9\u81f3\u652f\u63f4\u7cfb\u7d71\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - {reason}" + } + }, "system_health": { "info": { "agent_version": "Agent \u7248\u672c", diff --git a/homeassistant/components/here_travel_time/translations/pl.json b/homeassistant/components/here_travel_time/translations/pl.json index 17b91417007409..e409971f3b3d71 100644 --- a/homeassistant/components/here_travel_time/translations/pl.json +++ b/homeassistant/components/here_travel_time/translations/pl.json @@ -72,7 +72,7 @@ "init": { "data": { "route_mode": "Tryb trasy", - "traffic_mode": "Tryb ruchu", + "traffic_mode": "Tryb nat\u0119\u017cenia ruchu", "unit_system": "System metryczny" } }, diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json index f5716dcf951490..e3acc778fd3fd6 100644 --- a/homeassistant/components/motioneye/translations/bg.json +++ b/homeassistant/components/motioneye/translations/bg.json @@ -13,7 +13,10 @@ "step": { "user": { "data": { - "admin_password": "\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0441\u043a\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", + "admin_password": "\u041f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "admin_username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430", + "surveillance_password": "\u041f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435", + "surveillance_username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0437\u0430 \u043d\u0430\u0431\u043b\u044e\u0434\u0435\u043d\u0438\u0435", "url": "URL" } } diff --git a/homeassistant/components/ovo_energy/translations/ca.json b/homeassistant/components/ovo_energy/translations/ca.json index 0d0677ec522d3b..87278926984ab4 100644 --- a/homeassistant/components/ovo_energy/translations/ca.json +++ b/homeassistant/components/ovo_energy/translations/ca.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "Identificador de compte OVO (afegeix-lo nom\u00e9s si tens diversos comptes)", "password": "Contrasenya", "username": "Nom d'usuari" }, diff --git a/homeassistant/components/ovo_energy/translations/it.json b/homeassistant/components/ovo_energy/translations/it.json index be21bde566574c..c5069e5da862c7 100644 --- a/homeassistant/components/ovo_energy/translations/it.json +++ b/homeassistant/components/ovo_energy/translations/it.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID account OVO (aggiungi solo se hai pi\u00f9 account)", "password": "Password", "username": "Nome utente" }, diff --git a/homeassistant/components/ovo_energy/translations/pl.json b/homeassistant/components/ovo_energy/translations/pl.json index c426d1404177e3..a14ae9d2b6b73f 100644 --- a/homeassistant/components/ovo_energy/translations/pl.json +++ b/homeassistant/components/ovo_energy/translations/pl.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "Identyfikator konta OVO (dodaj tylko, je\u015bli masz wiele kont)", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, diff --git a/homeassistant/components/scrape/translations/ca.json b/homeassistant/components/scrape/translations/ca.json index ff6a0dba168908..2503602ab332f0 100644 --- a/homeassistant/components/scrape/translations/ca.json +++ b/homeassistant/components/scrape/translations/ca.json @@ -36,6 +36,11 @@ } } }, + "issues": { + "moved_yaml": { + "title": "La configuraci\u00f3 YAML de Scrape s'ha eliminat" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/it.json b/homeassistant/components/scrape/translations/it.json index e64ee3022d86aa..32d2fdcbd31e27 100644 --- a/homeassistant/components/scrape/translations/it.json +++ b/homeassistant/components/scrape/translations/it.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "La configurazione di Scrape tramite YAML \u00e8 stata spostata nella chiave di integrazione. \n\nLa tua configurazione YAML esistente funzioner\u00e0 per altre 2 versioni. \n\nMigra la tua configurazione YAML alla chiave di integrazione in base alla documentazione.", + "title": "La configurazione YAML di Scrape \u00e8 stata spostata" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/pl.json b/homeassistant/components/scrape/translations/pl.json index 67b2a3db685745..aa551e8f08c346 100644 --- a/homeassistant/components/scrape/translations/pl.json +++ b/homeassistant/components/scrape/translations/pl.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "Konfiguracja Scrape za pomoc\u0105 YAML zosta\u0142a przeniesiona do klucza integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML b\u0119dzie dzia\u0142a\u0107 przez kolejne 2 wersje. \n\nPrzenie\u015b swoj\u0105 konfiguracj\u0119 YAML do klucza integracji zgodnie z dokumentacj\u0105.", + "title": "Konfiguracja YAML dla Scrape zostaje przeniesiona" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/scrape/translations/zh-Hant.json b/homeassistant/components/scrape/translations/zh-Hant.json index 499ca44d3345e8..d188fc7894e722 100644 --- a/homeassistant/components/scrape/translations/zh-Hant.json +++ b/homeassistant/components/scrape/translations/zh-Hant.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Scrape \u5373\u5c07\u8f49\u79fb\u81f3\u6574\u5408\u3002\n\n\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u53ea\u80fd\u518d\u4f7f\u7528\u5169\u500b\u66f4\u65b0\u7248\u672c\u3002\n\n\u8ddf\u96a8\u6587\u4ef6\u8aaa\u660e\u9077\u79fb YAML \u8a2d\u5b9a\u81f3\u6574\u5408\u3002", + "title": "Scrape YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index c43cb84d5f65e3..86e02b8cc07ead 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -10,6 +10,9 @@ "incorrect_validation_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434 \u0437\u0430 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435" }, "step": { + "two_factor": { + "description": "\u0418\u0437\u0438\u0441\u043a\u0432\u0430 \u0441\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "country": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u044a\u0440\u0436\u0430\u0432\u0430", diff --git a/homeassistant/components/transmission/translations/it.json b/homeassistant/components/transmission/translations/it.json index 2cefcdfb290f47..41eaa4efdf5052 100644 --- a/homeassistant/components/transmission/translations/it.json +++ b/homeassistant/components/transmission/translations/it.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna eventuali automazioni o script che utilizzano questo servizio e sostituisci la chiave del nome con la chiave entry_id.", + "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" + } + } + }, + "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index 994744a3547f2e..7ae84ea4f4eb82 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi i zast\u0105p klucz nazwy kluczem entry_id.", + "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" + } + } + }, + "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index fd3d3a909aaab1..235a13f2c01db0 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3 name key \u70ba entry_id key\u3002", + "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + } + }, + "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { From 10aa1d386a22e926edeb53122be16896cf5179c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 19:52:13 -0500 Subject: [PATCH 161/394] Bump dbus-fast to 1.61.1 (#81386) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9c2438b8b189ee..89281323541cdb 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.60.0" + "dbus-fast==1.61.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index db85880ffbd3d2..668f30a67d93c3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.60.0 +dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 50c5079438404a..e8c59a1162e02a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.60.0 +dbus-fast==1.61.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0230fa1d27418..f0b688f4a63608 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.60.0 +dbus-fast==1.61.1 # homeassistant.components.debugpy debugpy==1.6.3 From 0bfb0c25f6ff9a108aa9cf71d3001042f49a0380 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 1 Nov 2022 21:29:11 -0400 Subject: [PATCH 162/394] Improve supervisor repairs (#81387) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/repairs.py | 59 +++++++++- homeassistant/components/hassio/strings.json | 104 +++++++++++++++++- .../components/hassio/translations/en.json | 104 +++++++++++++++++- tests/components/hassio/test_repairs.py | 77 ++++++++++++- 4 files changed, 330 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index a8c6788f4d5983..21120d8d52283a 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -36,6 +36,39 @@ INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy" INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported" +UNSUPPORTED_REASONS = { + "apparmor", + "connectivity_check", + "content_trust", + "dbus", + "dns_server", + "docker_configuration", + "docker_version", + "cgroup_version", + "job_conditions", + "lxc", + "network_manager", + "os", + "os_agent", + "restart_policy", + "software", + "source_mods", + "supervisor_version", + "systemd", + "systemd_journal", + "systemd_resolved", +} +# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason +# provides no additional information beyond the unhealthy one then skip that repair. +UNSUPPORTED_SKIP_REPAIR = {"privileged"} +UNHEALTHY_REASONS = { + "docker", + "supervisor", + "setup", + "privileged", + "untrusted", +} + class SupervisorRepairs: """Create repairs from supervisor events.""" @@ -56,6 +89,13 @@ def unhealthy_reasons(self) -> set[str]: def unhealthy_reasons(self, reasons: set[str]) -> None: """Set unhealthy reasons. Create or delete repairs as necessary.""" for unhealthy in reasons - self.unhealthy_reasons: + if unhealthy in UNHEALTHY_REASONS: + translation_key = f"unhealthy_{unhealthy}" + translation_placeholders = None + else: + translation_key = "unhealthy" + translation_placeholders = {"reason": unhealthy} + async_create_issue( self._hass, DOMAIN, @@ -63,8 +103,8 @@ def unhealthy_reasons(self, reasons: set[str]) -> None: is_fixable=False, learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}", severity=IssueSeverity.CRITICAL, - translation_key="unhealthy", - translation_placeholders={"reason": unhealthy}, + translation_key=translation_key, + translation_placeholders=translation_placeholders, ) for fixed in self.unhealthy_reasons - reasons: @@ -80,7 +120,14 @@ def unsupported_reasons(self) -> set[str]: @unsupported_reasons.setter def unsupported_reasons(self, reasons: set[str]) -> None: """Set unsupported reasons. Create or delete repairs as necessary.""" - for unsupported in reasons - self.unsupported_reasons: + for unsupported in reasons - UNSUPPORTED_SKIP_REPAIR - self.unsupported_reasons: + if unsupported in UNSUPPORTED_REASONS: + translation_key = f"unsupported_{unsupported}" + translation_placeholders = None + else: + translation_key = "unsupported" + translation_placeholders = {"reason": unsupported} + async_create_issue( self._hass, DOMAIN, @@ -88,11 +135,11 @@ def unsupported_reasons(self, reasons: set[str]) -> None: is_fixable=False, learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}", severity=IssueSeverity.WARNING, - translation_key="unsupported", - translation_placeholders={"reason": unsupported}, + translation_key=translation_key, + translation_placeholders=translation_placeholders, ) - for fixed in self.unsupported_reasons - reasons: + for fixed in self.unsupported_reasons - (reasons - UNSUPPORTED_SKIP_REPAIR): async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}") self._unsupported_reasons = reasons diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 81b5ce01b79f57..7cda053f43a426 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -19,11 +19,111 @@ "issues": { "unhealthy": { "title": "Unhealthy system - {reason}", - "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it." + "description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this." + }, + "unhealthy_docker": { + "title": "Unhealthy system - Docker misconfigured", + "description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this." + }, + "unhealthy_supervisor": { + "title": "Unhealthy system - Supervisor update failed", + "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this." + }, + "unhealthy_setup": { + "title": "Unhealthy system - Setup failed", + "description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this." + }, + "unhealthy_privileged": { + "title": "Unhealthy system - Not privileged", + "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this." + }, + "unhealthy_untrusted": { + "title": "Unhealthy system - Untrusted code", + "description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this." }, "unsupported": { "title": "Unsupported system - {reason}", - "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system." + "description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this." + }, + "unsupported_apparmor": { + "title": "Unsupported system - AppArmor issues", + "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this." + }, + "unsupported_cgroup_version": { + "title": "Unsupported system - CGroup version", + "description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this." + }, + "unsupported_connectivity_check": { + "title": "Unsupported system - Connectivity check disabled", + "description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this." + }, + "unsupported_content_trust": { + "title": "Unsupported system - Content-trust check disabled", + "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this." + }, + "unsupported_dbus": { + "title": "Unsupported system - D-Bus issues", + "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this." + }, + "unsupported_dns_server": { + "title": "Unsupported system - DNS server issues", + "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this." + }, + "unsupported_docker_configuration": { + "title": "Unsupported system - Docker misconfigured", + "description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this." + }, + "unsupported_docker_version": { + "title": "Unsupported system - Docker version", + "description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this." + }, + "unsupported_job_conditions": { + "title": "Unsupported system - Protections disabled", + "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this." + }, + "unsupported_lxc": { + "title": "Unsupported system - LXC detected", + "description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this." + }, + "unsupported_network_manager": { + "title": "Unsupported system - Network Manager issues", + "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_os": { + "title": "Unsupported system - Operating System", + "description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this." + }, + "unsupported_os_agent": { + "title": "Unsupported system - OS-Agent issues", + "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_restart_policy": { + "title": "Unsupported system - Container restart policy", + "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this." + }, + "unsupported_software": { + "title": "Unsupported system - Unsupported software", + "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this." + }, + "unsupported_source_mods": { + "title": "Unsupported system - Supervisor source modifications", + "description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this." + }, + "unsupported_supervisor_version": { + "title": "Unsupported system - Supervisor version", + "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this." + }, + "unsupported_systemd": { + "title": "Unsupported system - Systemd issues", + "description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_systemd_journal": { + "title": "Unsupported system - Systemd Journal issues", + "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this." + }, + "unsupported_systemd_resolved": { + "title": "Unsupported system - Systemd-Resolved issues", + "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this." } } } diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index b6f006e30932c5..243467b9f228af 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it.", + "description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this.", "title": "Unhealthy system - {reason}" }, + "unhealthy_docker": { + "description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Docker misconfigured" + }, + "unhealthy_privileged": { + "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Not privileged" + }, + "unhealthy_setup": { + "description": "System is currently because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.", + "title": "Unhealthy system - Setup failed" + }, + "unhealthy_supervisor": { + "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Supervisor update failed" + }, + "unhealthy_untrusted": { + "description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Untrusted code" + }, "unsupported": { - "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system.", + "description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this.", "title": "Unsupported system - {reason}" + }, + "unsupported_apparmor": { + "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this.", + "title": "Unsupported system - AppArmor issues" + }, + "unsupported_cgroup_version": { + "description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this.", + "title": "Unsupported system - CGroup version" + }, + "unsupported_connectivity_check": { + "description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Connectivity check disabled" + }, + "unsupported_content_trust": { + "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Content-trust check disabled" + }, + "unsupported_dbus": { + "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this.", + "title": "Unsupported system - D-Bus issues" + }, + "unsupported_dns_server": { + "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this.", + "title": "Unsupported system - DNS server issues" + }, + "unsupported_docker_configuration": { + "description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Docker misconfigured" + }, + "unsupported_docker_version": { + "description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this.", + "title": "Unsupported system - Docker version" + }, + "unsupported_job_conditions": { + "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Protections disabled" + }, + "unsupported_lxc": { + "description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this.", + "title": "Unsupported system - LXC detected" + }, + "unsupported_network_manager": { + "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Network Manager issues" + }, + "unsupported_os": { + "description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this.", + "title": "Unsupported system - Operating System" + }, + "unsupported_os_agent": { + "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - OS-Agent issues" + }, + "unsupported_restart_policy": { + "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Container restart policy" + }, + "unsupported_software": { + "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Unsupported software" + }, + "unsupported_source_mods": { + "description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Supervisor source modifications" + }, + "unsupported_supervisor_version": { + "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Supervisor version" + }, + "unsupported_systemd": { + "description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd issues" + }, + "unsupported_systemd_journal": { + "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd Journal issues" + }, + "unsupported_systemd_resolved": { + "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd-Resolved issues" } }, "system_health": { diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index ebaf46be3b597a..f420e926b0906a 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -140,10 +140,8 @@ def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: "issue_domain": None, "learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}", "severity": "critical" if unhealthy else "warning", - "translation_key": repair_type, - "translation_placeholders": { - "reason": reason, - }, + "translation_key": f"{repair_type}_{reason}", + "translation_placeholders": None, } in issues @@ -393,3 +391,74 @@ async def test_reasons_added_and_removed( assert_repair_in_list( msg["result"]["issues"], unhealthy=False, reason="content_trust" ) + + +async def test_ignored_unsupported_skipped( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Unsupported reasons which have an identical unhealthy reason are ignored.""" + mock_resolution_info( + aioclient_mock, unsupported=["privileged"], unhealthy=["privileged"] + ) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="privileged") + + +async def test_new_unsupported_unhealthy_reason( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """New unsupported/unhealthy reasons result in a generic repair until next core update.""" + mock_resolution_info( + aioclient_mock, unsupported=["fake_unsupported"], unhealthy=["fake_unhealthy"] + ) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": "unhealthy_system_fake_unhealthy", + "issue_domain": None, + "learn_more_url": "https://www.home-assistant.io/more-info/unhealthy/fake_unhealthy", + "severity": "critical", + "translation_key": "unhealthy", + "translation_placeholders": {"reason": "fake_unhealthy"}, + } in msg["result"]["issues"] + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": "unsupported_system_fake_unsupported", + "issue_domain": None, + "learn_more_url": "https://www.home-assistant.io/more-info/unsupported/fake_unsupported", + "severity": "warning", + "translation_key": "unsupported", + "translation_placeholders": {"reason": "fake_unsupported"}, + } in msg["result"]["issues"] From f445b96a4ea0232805062e703153752379131240 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Nov 2022 05:08:16 -0500 Subject: [PATCH 163/394] Bump aiohomekit to 2.2.13 (#81398) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 09f2a15871f822..18884d59307970 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.12"], + "requirements": ["aiohomekit==2.2.13"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index e8c59a1162e02a..b83434bc3c2269 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.12 +aiohomekit==2.2.13 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0b688f4a63608..2565b535db2a98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.12 +aiohomekit==2.2.13 # homeassistant.components.emulated_hue # homeassistant.components.http From 44f63252e7266839072840db4c16fd73c0106bd9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Nov 2022 11:52:19 +0100 Subject: [PATCH 164/394] Update frontend to 20221102.0 (#81405) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index aed26eb5de1ea4..be97ceee52292d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221031.0"], + "requirements": ["home-assistant-frontend==20221102.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 668f30a67d93c3..5b5ee9c0e7b019 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index b83434bc3c2269..581f3cd12fdb80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2565b535db2a98..c7aad37a8ad2f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -651,7 +651,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 # homeassistant.components.home_connect homeconnect==0.7.2 From a8c527f6f3994f36edc71b59e5cee1136cef7f23 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Nov 2022 07:18:50 -0400 Subject: [PATCH 165/394] Add unit conversion for energy costs (#81379) Co-authored-by: Franck Nijhof --- homeassistant/components/energy/sensor.py | 107 +++++++++++++--------- tests/components/energy/test_sensor.py | 24 ++++- 2 files changed, 82 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index c97b67287d14b1..71e385f2fec2b8 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import copy from dataclasses import dataclass import logging @@ -22,6 +23,7 @@ VOLUME_GALLONS, VOLUME_LITERS, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import ( HomeAssistant, @@ -34,29 +36,35 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import unit_conversion import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM from .const import DOMAIN from .data import EnergyManager, async_get_manager -SUPPORTED_STATE_CLASSES = [ +SUPPORTED_STATE_CLASSES = { SensorStateClass.MEASUREMENT, SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, -] -VALID_ENERGY_UNITS = [ +} +VALID_ENERGY_UNITS: set[str] = { UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.GIGA_JOULE, -] -VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS -VALID_VOLUME_UNITS_WATER = [ +} +VALID_ENERGY_UNITS_GAS = { + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, + *VALID_ENERGY_UNITS, +} +VALID_VOLUME_UNITS_WATER = { VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, VOLUME_GALLONS, VOLUME_LITERS, -] +} _LOGGER = logging.getLogger(__name__) @@ -252,8 +260,24 @@ def _reset(self, energy_state: State) -> None: self.async_write_ha_state() @callback - def _update_cost(self) -> None: # noqa: C901 + def _update_cost(self) -> None: """Update incurred costs.""" + if self._adapter.source_type == "grid": + valid_units = VALID_ENERGY_UNITS + default_price_unit: str | None = UnitOfEnergy.KILO_WATT_HOUR + + elif self._adapter.source_type == "gas": + valid_units = VALID_ENERGY_UNITS_GAS + # No conversion for gas. + default_price_unit = None + + elif self._adapter.source_type == "water": + valid_units = VALID_VOLUME_UNITS_WATER + if self.hass.config.units is METRIC_SYSTEM: + default_price_unit = UnitOfVolume.CUBIC_METERS + else: + default_price_unit = UnitOfVolume.GALLONS + energy_state = self.hass.states.get( cast(str, self._config[self._adapter.stat_energy_key]) ) @@ -298,52 +322,27 @@ def _update_cost(self) -> None: # noqa: C901 except ValueError: return - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.WATT_HOUR}" - ): - energy_price *= 1000.0 - - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.MEGA_WATT_HOUR}" - ): - energy_price /= 1000.0 + energy_price_unit: str | None = energy_price_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, "" + ).partition("/")[2] - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.GIGA_JOULE}" - ): - energy_price /= 1000 / 3.6 + # For backwards compatibility we don't validate the unit of the price + # If it is not valid, we assume it's our default price unit. + if energy_price_unit not in valid_units: + energy_price_unit = default_price_unit else: - energy_price_state = None energy_price = cast(float, self._config["number_energy_price"]) + energy_price_unit = default_price_unit if self._last_energy_sensor_state is None: # Initialize as it's the first time all required entities are in place. self._reset(energy_state) return - energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - - if self._adapter.source_type == "grid": - if energy_unit not in VALID_ENERGY_UNITS: - energy_unit = None - - elif self._adapter.source_type == "gas": - if energy_unit not in VALID_ENERGY_UNITS_GAS: - energy_unit = None - - elif self._adapter.source_type == "water": - if energy_unit not in VALID_VOLUME_UNITS_WATER: - energy_unit = None - - if energy_unit == UnitOfEnergy.WATT_HOUR: - energy_price /= 1000 - elif energy_unit == UnitOfEnergy.MEGA_WATT_HOUR: - energy_price *= 1000 - elif energy_unit == UnitOfEnergy.GIGA_JOULE: - energy_price *= 1000 / 3.6 + energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if energy_unit is None: + if energy_unit is None or energy_unit not in valid_units: if not self._wrong_unit_reported: self._wrong_unit_reported = True _LOGGER.warning( @@ -373,10 +372,30 @@ def _update_cost(self) -> None: # noqa: C901 energy_state_copy = copy.copy(energy_state) energy_state_copy.state = "0.0" self._reset(energy_state_copy) + # Update with newly incurred cost old_energy_value = float(self._last_energy_sensor_state.state) cur_value = cast(float, self._attr_native_value) - self._attr_native_value = cur_value + (energy - old_energy_value) * energy_price + + if energy_price_unit is None: + converted_energy_price = energy_price + else: + if self._adapter.source_type == "grid": + converter: Callable[ + [float, str, str], float + ] = unit_conversion.EnergyConverter.convert + elif self._adapter.source_type in ("gas", "water"): + converter = unit_conversion.VolumeConverter.convert + + converted_energy_price = converter( + energy_price, + energy_unit, + energy_price_unit, + ) + + self._attr_native_value = ( + cur_value + (energy - old_energy_value) * converted_energy_price + ) self._last_energy_sensor_state = energy_state diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 14a04ea74c6326..0108dd1de7693c 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -19,11 +19,13 @@ STATE_UNKNOWN, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, + VOLUME_GALLONS, UnitOfEnergy, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.components.recorder.common import async_wait_recording_done @@ -832,7 +834,10 @@ async def test_cost_sensor_handle_price_units( assert state.state == "20.0" -@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +@pytest.mark.parametrize( + "unit", + (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), +) async def test_cost_sensor_handle_gas( setup_integration, hass, hass_storage, unit ) -> None: @@ -933,13 +938,22 @@ async def test_cost_sensor_handle_gas_kwh( assert state.state == "50.0" -@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +@pytest.mark.parametrize( + "unit_system,usage_unit,growth", + ( + # 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3: + (US_CUSTOMARY_SYSTEM, VOLUME_CUBIC_FEET, 374.025974025974), + (US_CUSTOMARY_SYSTEM, VOLUME_GALLONS, 50.0), + (METRIC_SYSTEM, VOLUME_CUBIC_METERS, 50.0), + ), +) async def test_cost_sensor_handle_water( - setup_integration, hass, hass_storage, unit + setup_integration, hass, hass_storage, unit_system, usage_unit, growth ) -> None: """Test water cost price from sensor entity.""" + hass.config.units = unit_system energy_attributes = { - ATTR_UNIT_OF_MEASUREMENT: unit, + ATTR_UNIT_OF_MEASUREMENT: usage_unit, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() @@ -981,7 +995,7 @@ async def test_cost_sensor_handle_water( await hass.async_block_till_done() state = hass.states.get("sensor.water_consumption_cost") - assert state.state == "50.0" + assert float(state.state) == pytest.approx(growth) @pytest.mark.parametrize("state_class", [None]) From b9132e78b48bc9e0356c3784ae6d357e25cd0db3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Nov 2022 19:11:50 +0100 Subject: [PATCH 166/394] Improve error logging of WebSocket API (#81360) --- .../components/websocket_api/connection.py | 12 ++- .../websocket_api/test_connection.py | 92 +++++++++++++++---- 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index c344e1c6a9fd4e..ab4dda845db9f7 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User +from homeassistant.components.http import current_request from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized @@ -137,6 +138,13 @@ def async_handle_exception(self, msg: dict[str, Any], err: Exception) -> None: err_message = "Unknown error" log_handler = self.logger.exception - log_handler("Error handling message: %s (%s)", err_message, code) - self.send_message(messages.error_message(msg["id"], code, err_message)) + + if code: + err_message += f" ({code})" + if request := current_request.get(): + err_message += f" from {request.remote}" + if user_agent := request.headers.get("user-agent"): + err_message += f" ({user_agent})" + + log_handler("Error handling message: %s", err_message) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index fd9af99c1a48d3..8f2cd43fdb8b76 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -1,8 +1,11 @@ """Test WebSocket Connection class.""" import asyncio import logging -from unittest.mock import Mock +from typing import Any +from unittest.mock import AsyncMock, Mock, patch +from aiohttp.test_utils import make_mocked_request +import pytest import voluptuous as vol from homeassistant import exceptions @@ -11,37 +14,86 @@ from tests.common import MockUser -async def test_exception_handling(): - """Test handling of exceptions.""" - send_messages = [] - user = MockUser() - refresh_token = Mock() - conn = websocket_api.ActiveConnection( - logging.getLogger(__name__), None, send_messages.append, user, refresh_token - ) - - for (exc, code, err) in ( - (exceptions.Unauthorized(), websocket_api.ERR_UNAUTHORIZED, "Unauthorized"), +@pytest.mark.parametrize( + "exc,code,err,log", + [ + ( + exceptions.Unauthorized(), + websocket_api.ERR_UNAUTHORIZED, + "Unauthorized", + "Error handling message: Unauthorized (unauthorized) from 127.0.0.42 (Browser)", + ), ( vol.Invalid("Invalid something"), websocket_api.ERR_INVALID_FORMAT, "Invalid something. Got {'id': 5}", + "Error handling message: Invalid something. Got {'id': 5} (invalid_format) from 127.0.0.42 (Browser)", + ), + ( + asyncio.TimeoutError(), + websocket_api.ERR_TIMEOUT, + "Timeout", + "Error handling message: Timeout (timeout) from 127.0.0.42 (Browser)", ), - (asyncio.TimeoutError(), websocket_api.ERR_TIMEOUT, "Timeout"), ( exceptions.HomeAssistantError("Failed to do X"), websocket_api.ERR_UNKNOWN_ERROR, "Failed to do X", + "Error handling message: Failed to do X (unknown_error) from 127.0.0.42 (Browser)", + ), + ( + ValueError("Really bad"), + websocket_api.ERR_UNKNOWN_ERROR, + "Unknown error", + "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)", ), - (ValueError("Really bad"), websocket_api.ERR_UNKNOWN_ERROR, "Unknown error"), ( - exceptions.HomeAssistantError(), + exceptions.HomeAssistantError, websocket_api.ERR_UNKNOWN_ERROR, "Unknown error", + "Error handling message: Unknown error (unknown_error) from 127.0.0.42 (Browser)", ), - ): - send_messages.clear() + ], +) +async def test_exception_handling( + caplog: pytest.LogCaptureFixture, + exc: Exception, + code: str, + err: str, + log: str, +): + """Test handling of exceptions.""" + send_messages = [] + user = MockUser() + refresh_token = Mock() + current_request = AsyncMock() + + def get_extra_info(key: str) -> Any: + if key == "sslcontext": + return True + + if key == "peername": + return ("127.0.0.42", 8123) + + mocked_transport = Mock() + mocked_transport.get_extra_info = get_extra_info + mocked_request = make_mocked_request( + "GET", + "/api/websocket", + headers={"Host": "example.com", "User-Agent": "Browser"}, + transport=mocked_transport, + ) + + with patch( + "homeassistant.components.websocket_api.connection.current_request", + ) as current_request: + current_request.get.return_value = mocked_request + conn = websocket_api.ActiveConnection( + logging.getLogger(__name__), None, send_messages.append, user, refresh_token + ) + conn.async_handle_exception({"id": 5}, exc) - assert len(send_messages) == 1 - assert send_messages[0]["error"]["code"] == code - assert send_messages[0]["error"]["message"] == err + assert len(send_messages) == 1 + assert send_messages[0]["error"]["code"] == code + assert send_messages[0]["error"]["message"] == err + assert log in caplog.text From 95ce20638a1d91aaad7e6d53df09e0ec6c47e228 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 1 Nov 2022 13:57:53 -0400 Subject: [PATCH 167/394] Bump zigpy-zigate to 0.10.3 (#81363) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 79980d763e7f15..e40a54c11bce99 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -11,7 +11,7 @@ "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", - "zigpy-zigate==0.10.2", + "zigpy-zigate==0.10.3", "zigpy-znp==0.9.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index b751dc428ad182..bcd49db3368747 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2622,7 +2622,7 @@ zigpy-deconz==0.19.0 zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.2 +zigpy-zigate==0.10.3 # homeassistant.components.zha zigpy-znp==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd19d6861184d6..b1ff07c4c026ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1817,7 +1817,7 @@ zigpy-deconz==0.19.0 zigpy-xbee==0.16.2 # homeassistant.components.zha -zigpy-zigate==0.10.2 +zigpy-zigate==0.10.3 # homeassistant.components.zha zigpy-znp==0.9.1 From 0dbf0504ffc3b502ab151b32a8e788a930cf2a81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 15:49:05 -0500 Subject: [PATCH 168/394] Bump bleak-retry-connector to 2.8.2 (#81370) * Bump bleak-retry-connector to 2.8.2 Tweaks for the esp32 proxies now that we have better error reporting. This change improves the retry cases a bit with the new https://github.com/esphome/esphome/pull/3971 * empty --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index bca2f7f9a8d1c7..9c2438b8b189ee 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.1", + "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.60.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index adff342729de81..e57c6ae21582d6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 bleak==0.19.1 bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index bcd49db3368747..d4d71b0dc90e90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -413,7 +413,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1ff07c4c026ed..bf93af330f6e96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,7 +337,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.1 +bleak-retry-connector==2.8.2 # homeassistant.components.bluetooth bleak==0.19.1 From a5f209b219bea2b9c20cede33b7515fd3cc7733e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 17:00:04 -0500 Subject: [PATCH 169/394] Bump aiohomekit to 2.2.12 (#81372) * Bump aiohomekit to 2.2.12 Fixes a missing lock which was noticable on the esp32s since they disconnect right away when you ask for gatt notify. https://github.com/Jc2k/aiohomekit/compare/2.2.11...2.2.12 * empty --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 6533d7f29beaa7..09f2a15871f822 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.11"], + "requirements": ["aiohomekit==2.2.12"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index d4d71b0dc90e90..153675f0a0f45b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.11 +aiohomekit==2.2.12 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf93af330f6e96..24820de0b498eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.11 +aiohomekit==2.2.12 # homeassistant.components.emulated_hue # homeassistant.components.http From 3aca3763741443aca8ff4b6a534b4b5b17d7e47d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 2 Nov 2022 07:18:50 -0400 Subject: [PATCH 170/394] Add unit conversion for energy costs (#81379) Co-authored-by: Franck Nijhof --- homeassistant/components/energy/sensor.py | 107 +++++++++++++--------- tests/components/energy/test_sensor.py | 24 ++++- 2 files changed, 82 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index c97b67287d14b1..71e385f2fec2b8 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import copy from dataclasses import dataclass import logging @@ -22,6 +23,7 @@ VOLUME_GALLONS, VOLUME_LITERS, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import ( HomeAssistant, @@ -34,29 +36,35 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import unit_conversion import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM from .const import DOMAIN from .data import EnergyManager, async_get_manager -SUPPORTED_STATE_CLASSES = [ +SUPPORTED_STATE_CLASSES = { SensorStateClass.MEASUREMENT, SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, -] -VALID_ENERGY_UNITS = [ +} +VALID_ENERGY_UNITS: set[str] = { UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.GIGA_JOULE, -] -VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS -VALID_VOLUME_UNITS_WATER = [ +} +VALID_ENERGY_UNITS_GAS = { + VOLUME_CUBIC_FEET, + VOLUME_CUBIC_METERS, + *VALID_ENERGY_UNITS, +} +VALID_VOLUME_UNITS_WATER = { VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, VOLUME_GALLONS, VOLUME_LITERS, -] +} _LOGGER = logging.getLogger(__name__) @@ -252,8 +260,24 @@ def _reset(self, energy_state: State) -> None: self.async_write_ha_state() @callback - def _update_cost(self) -> None: # noqa: C901 + def _update_cost(self) -> None: """Update incurred costs.""" + if self._adapter.source_type == "grid": + valid_units = VALID_ENERGY_UNITS + default_price_unit: str | None = UnitOfEnergy.KILO_WATT_HOUR + + elif self._adapter.source_type == "gas": + valid_units = VALID_ENERGY_UNITS_GAS + # No conversion for gas. + default_price_unit = None + + elif self._adapter.source_type == "water": + valid_units = VALID_VOLUME_UNITS_WATER + if self.hass.config.units is METRIC_SYSTEM: + default_price_unit = UnitOfVolume.CUBIC_METERS + else: + default_price_unit = UnitOfVolume.GALLONS + energy_state = self.hass.states.get( cast(str, self._config[self._adapter.stat_energy_key]) ) @@ -298,52 +322,27 @@ def _update_cost(self) -> None: # noqa: C901 except ValueError: return - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.WATT_HOUR}" - ): - energy_price *= 1000.0 - - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.MEGA_WATT_HOUR}" - ): - energy_price /= 1000.0 + energy_price_unit: str | None = energy_price_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT, "" + ).partition("/")[2] - if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith( - f"/{UnitOfEnergy.GIGA_JOULE}" - ): - energy_price /= 1000 / 3.6 + # For backwards compatibility we don't validate the unit of the price + # If it is not valid, we assume it's our default price unit. + if energy_price_unit not in valid_units: + energy_price_unit = default_price_unit else: - energy_price_state = None energy_price = cast(float, self._config["number_energy_price"]) + energy_price_unit = default_price_unit if self._last_energy_sensor_state is None: # Initialize as it's the first time all required entities are in place. self._reset(energy_state) return - energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - - if self._adapter.source_type == "grid": - if energy_unit not in VALID_ENERGY_UNITS: - energy_unit = None - - elif self._adapter.source_type == "gas": - if energy_unit not in VALID_ENERGY_UNITS_GAS: - energy_unit = None - - elif self._adapter.source_type == "water": - if energy_unit not in VALID_VOLUME_UNITS_WATER: - energy_unit = None - - if energy_unit == UnitOfEnergy.WATT_HOUR: - energy_price /= 1000 - elif energy_unit == UnitOfEnergy.MEGA_WATT_HOUR: - energy_price *= 1000 - elif energy_unit == UnitOfEnergy.GIGA_JOULE: - energy_price *= 1000 / 3.6 + energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if energy_unit is None: + if energy_unit is None or energy_unit not in valid_units: if not self._wrong_unit_reported: self._wrong_unit_reported = True _LOGGER.warning( @@ -373,10 +372,30 @@ def _update_cost(self) -> None: # noqa: C901 energy_state_copy = copy.copy(energy_state) energy_state_copy.state = "0.0" self._reset(energy_state_copy) + # Update with newly incurred cost old_energy_value = float(self._last_energy_sensor_state.state) cur_value = cast(float, self._attr_native_value) - self._attr_native_value = cur_value + (energy - old_energy_value) * energy_price + + if energy_price_unit is None: + converted_energy_price = energy_price + else: + if self._adapter.source_type == "grid": + converter: Callable[ + [float, str, str], float + ] = unit_conversion.EnergyConverter.convert + elif self._adapter.source_type in ("gas", "water"): + converter = unit_conversion.VolumeConverter.convert + + converted_energy_price = converter( + energy_price, + energy_unit, + energy_price_unit, + ) + + self._attr_native_value = ( + cur_value + (energy - old_energy_value) * converted_energy_price + ) self._last_energy_sensor_state = energy_state diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 14a04ea74c6326..0108dd1de7693c 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -19,11 +19,13 @@ STATE_UNKNOWN, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, + VOLUME_GALLONS, UnitOfEnergy, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.components.recorder.common import async_wait_recording_done @@ -832,7 +834,10 @@ async def test_cost_sensor_handle_price_units( assert state.state == "20.0" -@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +@pytest.mark.parametrize( + "unit", + (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), +) async def test_cost_sensor_handle_gas( setup_integration, hass, hass_storage, unit ) -> None: @@ -933,13 +938,22 @@ async def test_cost_sensor_handle_gas_kwh( assert state.state == "50.0" -@pytest.mark.parametrize("unit", (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS)) +@pytest.mark.parametrize( + "unit_system,usage_unit,growth", + ( + # 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3: + (US_CUSTOMARY_SYSTEM, VOLUME_CUBIC_FEET, 374.025974025974), + (US_CUSTOMARY_SYSTEM, VOLUME_GALLONS, 50.0), + (METRIC_SYSTEM, VOLUME_CUBIC_METERS, 50.0), + ), +) async def test_cost_sensor_handle_water( - setup_integration, hass, hass_storage, unit + setup_integration, hass, hass_storage, unit_system, usage_unit, growth ) -> None: """Test water cost price from sensor entity.""" + hass.config.units = unit_system energy_attributes = { - ATTR_UNIT_OF_MEASUREMENT: unit, + ATTR_UNIT_OF_MEASUREMENT: usage_unit, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, } energy_data = data.EnergyManager.default_preferences() @@ -981,7 +995,7 @@ async def test_cost_sensor_handle_water( await hass.async_block_till_done() state = hass.states.get("sensor.water_consumption_cost") - assert state.state == "50.0" + assert float(state.state) == pytest.approx(growth) @pytest.mark.parametrize("state_class", [None]) From 9f54e332ec68a348a00cb82c0759b1cd3b64aa25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Nov 2022 19:52:13 -0500 Subject: [PATCH 171/394] Bump dbus-fast to 1.61.1 (#81386) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 9c2438b8b189ee..89281323541cdb 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.60.0" + "dbus-fast==1.61.1" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e57c6ae21582d6..c29364f2466624 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.1 -dbus-fast==1.60.0 +dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 153675f0a0f45b..bf2de8bf27202f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.60.0 +dbus-fast==1.61.1 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24820de0b498eb..1eae9fc9ee3ea5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.60.0 +dbus-fast==1.61.1 # homeassistant.components.debugpy debugpy==1.6.3 From f6c094b017cdb4a8e8f0c164adfad63ee7df5552 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Tue, 1 Nov 2022 21:29:11 -0400 Subject: [PATCH 172/394] Improve supervisor repairs (#81387) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hassio/repairs.py | 59 +++++++++- homeassistant/components/hassio/strings.json | 104 +++++++++++++++++- .../components/hassio/translations/en.json | 104 +++++++++++++++++- tests/components/hassio/test_repairs.py | 77 ++++++++++++- 4 files changed, 330 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index a8c6788f4d5983..21120d8d52283a 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -36,6 +36,39 @@ INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy" INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported" +UNSUPPORTED_REASONS = { + "apparmor", + "connectivity_check", + "content_trust", + "dbus", + "dns_server", + "docker_configuration", + "docker_version", + "cgroup_version", + "job_conditions", + "lxc", + "network_manager", + "os", + "os_agent", + "restart_policy", + "software", + "source_mods", + "supervisor_version", + "systemd", + "systemd_journal", + "systemd_resolved", +} +# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason +# provides no additional information beyond the unhealthy one then skip that repair. +UNSUPPORTED_SKIP_REPAIR = {"privileged"} +UNHEALTHY_REASONS = { + "docker", + "supervisor", + "setup", + "privileged", + "untrusted", +} + class SupervisorRepairs: """Create repairs from supervisor events.""" @@ -56,6 +89,13 @@ def unhealthy_reasons(self) -> set[str]: def unhealthy_reasons(self, reasons: set[str]) -> None: """Set unhealthy reasons. Create or delete repairs as necessary.""" for unhealthy in reasons - self.unhealthy_reasons: + if unhealthy in UNHEALTHY_REASONS: + translation_key = f"unhealthy_{unhealthy}" + translation_placeholders = None + else: + translation_key = "unhealthy" + translation_placeholders = {"reason": unhealthy} + async_create_issue( self._hass, DOMAIN, @@ -63,8 +103,8 @@ def unhealthy_reasons(self, reasons: set[str]) -> None: is_fixable=False, learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}", severity=IssueSeverity.CRITICAL, - translation_key="unhealthy", - translation_placeholders={"reason": unhealthy}, + translation_key=translation_key, + translation_placeholders=translation_placeholders, ) for fixed in self.unhealthy_reasons - reasons: @@ -80,7 +120,14 @@ def unsupported_reasons(self) -> set[str]: @unsupported_reasons.setter def unsupported_reasons(self, reasons: set[str]) -> None: """Set unsupported reasons. Create or delete repairs as necessary.""" - for unsupported in reasons - self.unsupported_reasons: + for unsupported in reasons - UNSUPPORTED_SKIP_REPAIR - self.unsupported_reasons: + if unsupported in UNSUPPORTED_REASONS: + translation_key = f"unsupported_{unsupported}" + translation_placeholders = None + else: + translation_key = "unsupported" + translation_placeholders = {"reason": unsupported} + async_create_issue( self._hass, DOMAIN, @@ -88,11 +135,11 @@ def unsupported_reasons(self, reasons: set[str]) -> None: is_fixable=False, learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}", severity=IssueSeverity.WARNING, - translation_key="unsupported", - translation_placeholders={"reason": unsupported}, + translation_key=translation_key, + translation_placeholders=translation_placeholders, ) - for fixed in self.unsupported_reasons - reasons: + for fixed in self.unsupported_reasons - (reasons - UNSUPPORTED_SKIP_REPAIR): async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}") self._unsupported_reasons = reasons diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 81b5ce01b79f57..7cda053f43a426 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -19,11 +19,111 @@ "issues": { "unhealthy": { "title": "Unhealthy system - {reason}", - "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it." + "description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this." + }, + "unhealthy_docker": { + "title": "Unhealthy system - Docker misconfigured", + "description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this." + }, + "unhealthy_supervisor": { + "title": "Unhealthy system - Supervisor update failed", + "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this." + }, + "unhealthy_setup": { + "title": "Unhealthy system - Setup failed", + "description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this." + }, + "unhealthy_privileged": { + "title": "Unhealthy system - Not privileged", + "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this." + }, + "unhealthy_untrusted": { + "title": "Unhealthy system - Untrusted code", + "description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this." }, "unsupported": { "title": "Unsupported system - {reason}", - "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system." + "description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this." + }, + "unsupported_apparmor": { + "title": "Unsupported system - AppArmor issues", + "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this." + }, + "unsupported_cgroup_version": { + "title": "Unsupported system - CGroup version", + "description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this." + }, + "unsupported_connectivity_check": { + "title": "Unsupported system - Connectivity check disabled", + "description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this." + }, + "unsupported_content_trust": { + "title": "Unsupported system - Content-trust check disabled", + "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this." + }, + "unsupported_dbus": { + "title": "Unsupported system - D-Bus issues", + "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this." + }, + "unsupported_dns_server": { + "title": "Unsupported system - DNS server issues", + "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this." + }, + "unsupported_docker_configuration": { + "title": "Unsupported system - Docker misconfigured", + "description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this." + }, + "unsupported_docker_version": { + "title": "Unsupported system - Docker version", + "description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this." + }, + "unsupported_job_conditions": { + "title": "Unsupported system - Protections disabled", + "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this." + }, + "unsupported_lxc": { + "title": "Unsupported system - LXC detected", + "description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this." + }, + "unsupported_network_manager": { + "title": "Unsupported system - Network Manager issues", + "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_os": { + "title": "Unsupported system - Operating System", + "description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this." + }, + "unsupported_os_agent": { + "title": "Unsupported system - OS-Agent issues", + "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_restart_policy": { + "title": "Unsupported system - Container restart policy", + "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this." + }, + "unsupported_software": { + "title": "Unsupported system - Unsupported software", + "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this." + }, + "unsupported_source_mods": { + "title": "Unsupported system - Supervisor source modifications", + "description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this." + }, + "unsupported_supervisor_version": { + "title": "Unsupported system - Supervisor version", + "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this." + }, + "unsupported_systemd": { + "title": "Unsupported system - Systemd issues", + "description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this." + }, + "unsupported_systemd_journal": { + "title": "Unsupported system - Systemd Journal issues", + "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this." + }, + "unsupported_systemd_resolved": { + "title": "Unsupported system - Systemd-Resolved issues", + "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this." } } } diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index b6f006e30932c5..243467b9f228af 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "System is currently unhealthy due to '{reason}'. Use the link to learn more about what is wrong and how to fix it.", + "description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this.", "title": "Unhealthy system - {reason}" }, + "unhealthy_docker": { + "description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Docker misconfigured" + }, + "unhealthy_privileged": { + "description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Not privileged" + }, + "unhealthy_setup": { + "description": "System is currently because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.", + "title": "Unhealthy system - Setup failed" + }, + "unhealthy_supervisor": { + "description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Supervisor update failed" + }, + "unhealthy_untrusted": { + "description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this.", + "title": "Unhealthy system - Untrusted code" + }, "unsupported": { - "description": "System is unsupported due to '{reason}'. Use the link to learn more about what this means and how to return to a supported system.", + "description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this.", "title": "Unsupported system - {reason}" + }, + "unsupported_apparmor": { + "description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this.", + "title": "Unsupported system - AppArmor issues" + }, + "unsupported_cgroup_version": { + "description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this.", + "title": "Unsupported system - CGroup version" + }, + "unsupported_connectivity_check": { + "description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Connectivity check disabled" + }, + "unsupported_content_trust": { + "description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Content-trust check disabled" + }, + "unsupported_dbus": { + "description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this.", + "title": "Unsupported system - D-Bus issues" + }, + "unsupported_dns_server": { + "description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this.", + "title": "Unsupported system - DNS server issues" + }, + "unsupported_docker_configuration": { + "description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Docker misconfigured" + }, + "unsupported_docker_version": { + "description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this.", + "title": "Unsupported system - Docker version" + }, + "unsupported_job_conditions": { + "description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Protections disabled" + }, + "unsupported_lxc": { + "description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this.", + "title": "Unsupported system - LXC detected" + }, + "unsupported_network_manager": { + "description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Network Manager issues" + }, + "unsupported_os": { + "description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this.", + "title": "Unsupported system - Operating System" + }, + "unsupported_os_agent": { + "description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - OS-Agent issues" + }, + "unsupported_restart_policy": { + "description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Container restart policy" + }, + "unsupported_software": { + "description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Unsupported software" + }, + "unsupported_source_mods": { + "description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Supervisor source modifications" + }, + "unsupported_supervisor_version": { + "description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Supervisor version" + }, + "unsupported_systemd": { + "description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd issues" + }, + "unsupported_systemd_journal": { + "description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd Journal issues" + }, + "unsupported_systemd_resolved": { + "description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this.", + "title": "Unsupported system - Systemd-Resolved issues" } }, "system_health": { diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index ebaf46be3b597a..f420e926b0906a 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -140,10 +140,8 @@ def assert_repair_in_list(issues: list[dict[str, Any]], unhealthy: bool, reason: "issue_domain": None, "learn_more_url": f"https://www.home-assistant.io/more-info/{repair_type}/{reason}", "severity": "critical" if unhealthy else "warning", - "translation_key": repair_type, - "translation_placeholders": { - "reason": reason, - }, + "translation_key": f"{repair_type}_{reason}", + "translation_placeholders": None, } in issues @@ -393,3 +391,74 @@ async def test_reasons_added_and_removed( assert_repair_in_list( msg["result"]["issues"], unhealthy=False, reason="content_trust" ) + + +async def test_ignored_unsupported_skipped( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """Unsupported reasons which have an identical unhealthy reason are ignored.""" + mock_resolution_info( + aioclient_mock, unsupported=["privileged"], unhealthy=["privileged"] + ) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + assert_repair_in_list(msg["result"]["issues"], unhealthy=True, reason="privileged") + + +async def test_new_unsupported_unhealthy_reason( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client, +): + """New unsupported/unhealthy reasons result in a generic repair until next core update.""" + mock_resolution_info( + aioclient_mock, unsupported=["fake_unsupported"], unhealthy=["fake_unhealthy"] + ) + + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 2 + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": "unhealthy_system_fake_unhealthy", + "issue_domain": None, + "learn_more_url": "https://www.home-assistant.io/more-info/unhealthy/fake_unhealthy", + "severity": "critical", + "translation_key": "unhealthy", + "translation_placeholders": {"reason": "fake_unhealthy"}, + } in msg["result"]["issues"] + assert { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "hassio", + "ignored": False, + "is_fixable": False, + "issue_id": "unsupported_system_fake_unsupported", + "issue_domain": None, + "learn_more_url": "https://www.home-assistant.io/more-info/unsupported/fake_unsupported", + "severity": "warning", + "translation_key": "unsupported", + "translation_placeholders": {"reason": "fake_unsupported"}, + } in msg["result"]["issues"] From a6e745b6879baa2765d6750b72471bb3fe1f7e68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Nov 2022 05:08:16 -0500 Subject: [PATCH 173/394] Bump aiohomekit to 2.2.13 (#81398) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 09f2a15871f822..18884d59307970 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.12"], + "requirements": ["aiohomekit==2.2.13"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index bf2de8bf27202f..222ea1af1a4df3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.12 +aiohomekit==2.2.13 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1eae9fc9ee3ea5..a88e43e321c9c6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.12 +aiohomekit==2.2.13 # homeassistant.components.emulated_hue # homeassistant.components.http From 06d22d8249fbbfa91ef354d69637d5c6bebfde36 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Nov 2022 11:52:19 +0100 Subject: [PATCH 174/394] Update frontend to 20221102.0 (#81405) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index aed26eb5de1ea4..be97ceee52292d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221031.0"], + "requirements": ["home-assistant-frontend==20221102.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c29364f2466624..473b027edb7cd4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 222ea1af1a4df3..8ab3a60de07963 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a88e43e321c9c6..7be9395bbe3830 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221031.0 +home-assistant-frontend==20221102.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 3409dea28c4e5ba0726a6503301cbd9b99acba6d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Nov 2022 12:46:33 +0100 Subject: [PATCH 175/394] Bumped version to 2022.11.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 195b52c4debf25..532b8eee2ddf32 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index dd549dfeb0140a..c79195c95bd992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b6" +version = "2022.11.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From dd4b843d474ac585dc1cdd5bb12b0ccb242fafcf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Nov 2022 13:39:21 +0100 Subject: [PATCH 176/394] Use attr in mqtt number (#81399) --- homeassistant/components/mqtt/number.py | 50 +++++-------------------- 1 file changed, 9 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 25ef7af8d6eed5..95dbe970430f8a 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -12,7 +12,6 @@ DEFAULT_MIN_VALUE, DEFAULT_STEP, DEVICE_CLASSES_SCHEMA, - NumberDeviceClass, NumberMode, RestoreNumber, ) @@ -167,8 +166,6 @@ def __init__(self, hass, config, config_entry, discovery_data): self._optimistic = False self._sub_state = None - self._current_number = None - RestoreNumber.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -190,6 +187,12 @@ def _setup_from_config(self, config): entity=self, ).async_render_with_possible_json_value, } + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_mode = config[CONF_MODE] + self._attr_native_max_value = config[CONF_MAX] + self._attr_native_min_value = config[CONF_MIN] + self._attr_native_step = config[CONF_STEP] + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -222,7 +225,7 @@ def message_received(msg): ) return - self._current_number = num_value + self._attr_native_value = num_value get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: @@ -249,37 +252,7 @@ async def _subscribe_topics(self): if self._optimistic and ( last_number_data := await self.async_get_last_number_data() ): - self._current_number = last_number_data.native_value - - @property - def native_min_value(self) -> float: - """Return the minimum value.""" - return self._config[CONF_MIN] - - @property - def native_max_value(self) -> float: - """Return the maximum value.""" - return self._config[CONF_MAX] - - @property - def native_step(self) -> float: - """Return the increment/decrement step.""" - return self._config[CONF_STEP] - - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit of measurement.""" - return self._config.get(CONF_UNIT_OF_MEASUREMENT) - - @property - def native_value(self) -> float | None: - """Return the current value.""" - return self._current_number - - @property - def mode(self) -> NumberMode: - """Return the mode of the entity.""" - return self._config[CONF_MODE] + self._attr_native_value = last_number_data.native_value async def async_set_native_value(self, value: float) -> None: """Update the current value.""" @@ -290,7 +263,7 @@ async def async_set_native_value(self, value: float) -> None: payload = self._templates[CONF_COMMAND_TEMPLATE](current_number) if self._optimistic: - self._current_number = current_number + self._attr_native_value = current_number self.async_write_ha_state() await self.async_publish( @@ -305,8 +278,3 @@ async def async_set_native_value(self, value: float) -> None: def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - - @property - def device_class(self) -> NumberDeviceClass | None: - """Return the device class of the sensor.""" - return self._config.get(CONF_DEVICE_CLASS) From 7a930d7e79665eb1796213893e0c0ff0b3a47a87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Nov 2022 13:39:36 +0100 Subject: [PATCH 177/394] Use attr in mqtt humidifier (#81400) --- homeassistant/components/mqtt/humidifier.py | 59 +++++++-------------- 1 file changed, 18 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 7514f0ff672ddb..e3e94c07daeafc 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -211,10 +211,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT humidifier.""" - self._state = None - self._target_humidity = None - self._mode = None - self._supported_features = 0 + self._attr_mode = None self._topic = None self._payload = None @@ -265,10 +262,10 @@ def _setup_from_config(self, config): "MODE_RESET": config[CONF_PAYLOAD_RESET_MODE], } if CONF_MODE_COMMAND_TOPIC in config and CONF_AVAILABLE_MODES_LIST in config: - self._available_modes = config[CONF_AVAILABLE_MODES_LIST] + self._attr_available_modes = config[CONF_AVAILABLE_MODES_LIST] else: - self._available_modes = [] - if self._available_modes: + self._attr_available_modes = [] + if self._attr_available_modes: self._attr_supported_features = HumidifierEntityFeature.MODES else: self._attr_supported_features = 0 @@ -304,11 +301,11 @@ def state_received(msg): _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) return if payload == self._payload["STATE_ON"]: - self._state = True + self._attr_is_on = True elif payload == self._payload["STATE_OFF"]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: @@ -330,7 +327,7 @@ def target_humidity_received(msg): _LOGGER.debug("Ignoring empty target humidity from '%s'", msg.topic) return if rendered_target_humidity_payload == self._payload["HUMIDITY_RESET"]: - self._target_humidity = None + self._attr_target_humidity = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: @@ -354,7 +351,7 @@ def target_humidity_received(msg): rendered_target_humidity_payload, ) return - self._target_humidity = target_humidity + self._attr_target_humidity = target_humidity get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is not None: @@ -364,7 +361,7 @@ def target_humidity_received(msg): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._target_humidity = None + self._attr_target_humidity = None @callback @log_messages(self.hass, self.entity_id) @@ -372,7 +369,7 @@ def mode_received(msg): """Handle new received MQTT message for mode.""" mode = self._value_templates[ATTR_MODE](msg.payload) if mode == self._payload["MODE_RESET"]: - self._mode = None + self._attr_mode = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not mode: @@ -387,7 +384,7 @@ def mode_received(msg): ) return - self._mode = mode + self._attr_mode = mode get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_MODE_STATE_TOPIC] is not None: @@ -397,7 +394,7 @@ def mode_received(msg): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._mode = None + self._attr_mode = None self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, topics @@ -412,26 +409,6 @@ def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - @property - def available_modes(self) -> list: - """Get the list of available modes.""" - return self._available_modes - - @property - def is_on(self) -> bool | None: - """Return true if device is on.""" - return self._state - - @property - def target_humidity(self): - """Return the current target humidity.""" - return self._target_humidity - - @property - def mode(self): - """Return the current mode.""" - return self._mode - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the entity. @@ -446,7 +423,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: self._config[CONF_ENCODING], ) if self._optimistic: - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -463,7 +440,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: self._config[CONF_ENCODING], ) if self._optimistic: - self._state = False + self._attr_is_on = False self.async_write_ha_state() async def async_set_humidity(self, humidity: int) -> None: @@ -481,7 +458,7 @@ async def async_set_humidity(self, humidity: int) -> None: ) if self._optimistic_target_humidity: - self._target_humidity = humidity + self._attr_target_humidity = humidity self.async_write_ha_state() async def async_set_mode(self, mode: str) -> None: @@ -489,7 +466,7 @@ async def async_set_mode(self, mode: str) -> None: This method is a coroutine. """ - if mode not in self.available_modes: + if not self.available_modes or mode not in self.available_modes: _LOGGER.warning("'%s'is not a valid mode", mode) return @@ -504,5 +481,5 @@ async def async_set_mode(self, mode: str) -> None: ) if self._optimistic_mode: - self._mode = mode + self._attr_mode = mode self.async_write_ha_state() From a255546e9d76a95c7c670a6d6e761f5eefa49801 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Nov 2022 13:41:14 +0100 Subject: [PATCH 178/394] Use attr in mqtt binary sensor and switch (#81403) --- .../components/mqtt/binary_sensor.py | 28 +++++---------- homeassistant/components/mqtt/switch.py | 34 ++++++------------- 2 files changed, 19 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 915a2780283108..d9351908665df9 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, - BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -126,7 +125,6 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT binary sensor.""" - self._state: bool | None = None self._expiration_trigger = None self._delay_listener = None expire_after = config.get(CONF_EXPIRE_AFTER) @@ -154,7 +152,7 @@ async def mqtt_async_added_to_hass(self) -> None: _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) return self._expired = False - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at @@ -180,8 +178,10 @@ def config_schema(): """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: + self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_force_update = config[CONF_FORCE_UPDATE] + self._value_template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self, @@ -194,7 +194,7 @@ def _prepare_subscribe_topics(self): def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None - self._state = False + self._attr_is_on = False self.async_write_ha_state() @callback @@ -233,11 +233,11 @@ def state_message_received(msg): return if payload == self._config[CONF_PAYLOAD_ON]: - self._state = True + self._attr_is_on = True elif payload == self._config[CONF_PAYLOAD_OFF]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None else: # Payload is not for this entity template_info = "" if self._config.get(CONF_VALUE_TEMPLATE) is not None: @@ -256,7 +256,7 @@ def state_message_received(msg): self._delay_listener = None off_delay = self._config.get(CONF_OFF_DELAY) - if self._state and off_delay is not None: + if self._attr_is_on and off_delay is not None: self._delay_listener = evt.async_call_later( self.hass, off_delay, off_delay_listener ) @@ -288,16 +288,6 @@ def _value_is_expired(self, *_): self.async_write_ha_state() - @property - def is_on(self) -> bool | None: - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self) -> BinarySensorDeviceClass | None: - """Return the class of this sensor.""" - return self._config.get(CONF_DEVICE_CLASS) - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f8bf2f5bc6a8f7..f2a40facd8b411 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -7,11 +7,7 @@ import voluptuous as vol from homeassistant.components import switch -from homeassistant.components.switch import ( - DEVICE_CLASSES_SCHEMA, - SwitchDeviceClass, - SwitchEntity, -) +from homeassistant.components.switch import DEVICE_CLASSES_SCHEMA, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -125,8 +121,6 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" - self._state = None - self._state_on = None self._state_off = None self._optimistic = None @@ -138,8 +132,10 @@ def config_schema(): """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + state_on = config.get(CONF_STATE_ON) self._state_on = state_on if state_on else config[CONF_PAYLOAD_ON] @@ -163,11 +159,11 @@ def state_message_received(msg): """Handle new MQTT state messages.""" payload = self._value_template(msg.payload) if payload == self._state_on: - self._state = True + self._attr_is_on = True elif payload == self._state_off: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -193,23 +189,13 @@ async def _subscribe_topics(self): await subscription.async_subscribe_topics(self.hass, self._sub_state) if self._optimistic and (last_state := await self.async_get_last_state()): - self._state = last_state.state == STATE_ON - - @property - def is_on(self) -> bool | None: - """Return true if device is on.""" - return self._state + self._attr_is_on = last_state.state == STATE_ON @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - @property - def device_class(self) -> SwitchDeviceClass | None: - """Return the device class of the sensor.""" - return self._config.get(CONF_DEVICE_CLASS) - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on. @@ -224,7 +210,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: ) if self._optimistic: # Optimistically assume that switch has changed state. - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -241,5 +227,5 @@ async def async_turn_off(self, **kwargs: Any) -> None: ) if self._optimistic: # Optimistically assume that switch has changed state. - self._state = False + self._attr_is_on = False self.async_write_ha_state() From b2a4228dae6fa07e749fc278efd158a9d547498f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 2 Nov 2022 14:50:38 +0100 Subject: [PATCH 179/394] Update adax library to 0.1.5 (#81407) --- homeassistant/components/adax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 408c099b8ac5ee..cbe14f0d7a5273 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -3,7 +3,7 @@ "name": "Adax", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", - "requirements": ["adax==0.2.0", "Adax-local==0.1.4"], + "requirements": ["adax==0.2.0", "Adax-local==0.1.5"], "codeowners": ["@danielhiversen"], "iot_class": "local_polling", "loggers": ["adax", "adax_local"] diff --git a/requirements_all.txt b/requirements_all.txt index 581f3cd12fdb80..b056708b7918a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.2.1 AIOAladdinConnect==0.1.46 # homeassistant.components.adax -Adax-local==0.1.4 +Adax-local==0.1.5 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7aad37a8ad2f8..a977cfd710ef0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.2.1 AIOAladdinConnect==0.1.46 # homeassistant.components.adax -Adax-local==0.1.4 +Adax-local==0.1.5 # homeassistant.components.flick_electric PyFlick==0.0.2 From ab14e55c052433e42224199798b026637614685f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Nov 2022 14:57:59 +0100 Subject: [PATCH 180/394] Ensure we do not actually create a BleakScanner in the usage test (#81362) Avoids a failure when bluetooth is turned off when testing on macos: bleak.exc.BleakError: Bluetooth device is turned off --- tests/components/bluetooth/test_usage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 1bea3b149cd5b2..7e0d97d3d914e7 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -33,7 +33,8 @@ async def test_multiple_bleak_scanner_instances(hass): uninstall_multiple_bleak_catcher() - instance = bleak.BleakScanner() + with patch("bleak.get_platform_scanner_backend_type"): + instance = bleak.BleakScanner() assert not isinstance(instance, HaBleakScannerWrapper) From 71920cd6878ff9363477fa4ee97dc2807875e987 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Nov 2022 15:02:09 +0100 Subject: [PATCH 181/394] Update spotipy to 2.21.0 (#81395) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 0ce71f371df7ab..be662e969f2199 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.20.0"], + "requirements": ["spotipy==2.21.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["application_credentials"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index b056708b7918a0..dd54335dd7101b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2310,7 +2310,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.20.0 +spotipy==2.21.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a977cfd710ef0c..3ec5e21e4f693f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1595,7 +1595,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.20.0 +spotipy==2.21.0 # homeassistant.components.recorder # homeassistant.components.sql From fc3843f5e2de3a22d1d5170e747ee905524ec65b Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 2 Nov 2022 17:11:44 +0200 Subject: [PATCH 182/394] Add config flow to `pushbullet` (#74240) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 2 + .../components/pushbullet/__init__.py | 78 +++++++ homeassistant/components/pushbullet/api.py | 32 +++ .../components/pushbullet/config_flow.py | 63 +++++ homeassistant/components/pushbullet/const.py | 12 + .../components/pushbullet/manifest.json | 3 +- homeassistant/components/pushbullet/notify.py | 174 +++++++------- homeassistant/components/pushbullet/sensor.py | 144 ++++++------ .../components/pushbullet/strings.json | 25 ++ .../pushbullet/translations/en.json | 25 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- tests/components/pushbullet/__init__.py | 4 + tests/components/pushbullet/conftest.py | 28 +++ .../pushbullet/fixtures/channels.json | 14 ++ .../components/pushbullet/fixtures/chats.json | 18 ++ .../pushbullet/fixtures/user_info.json | 10 + .../components/pushbullet/test_config_flow.py | 134 +++++++++++ tests/components/pushbullet/test_init.py | 84 +++++++ tests/components/pushbullet/test_notify.py | 215 +++++------------- 21 files changed, 757 insertions(+), 312 deletions(-) create mode 100644 homeassistant/components/pushbullet/api.py create mode 100644 homeassistant/components/pushbullet/config_flow.py create mode 100644 homeassistant/components/pushbullet/const.py create mode 100644 homeassistant/components/pushbullet/strings.json create mode 100644 homeassistant/components/pushbullet/translations/en.json create mode 100644 tests/components/pushbullet/conftest.py create mode 100644 tests/components/pushbullet/fixtures/channels.json create mode 100644 tests/components/pushbullet/fixtures/chats.json create mode 100644 tests/components/pushbullet/fixtures/user_info.json create mode 100644 tests/components/pushbullet/test_config_flow.py create mode 100644 tests/components/pushbullet/test_init.py diff --git a/.coveragerc b/.coveragerc index 9d33acf6c75baf..b782f8444dbdcc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -999,6 +999,7 @@ omit = homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/pulseaudio_loopback/switch.py + homeassistant/components/pushbullet/api.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py homeassistant/components/pushover/notify.py diff --git a/CODEOWNERS b/CODEOWNERS index 4012868c712616..cb4e68f1535c74 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -880,6 +880,8 @@ build.json @home-assistant/supervisor /tests/components/pure_energie/ @klaasnicolaas /homeassistant/components/push/ @dgomes /tests/components/push/ @dgomes +/homeassistant/components/pushbullet/ @engrbm87 +/tests/components/pushbullet/ @engrbm87 /homeassistant/components/pushover/ @engrbm87 /tests/components/pushover/ @engrbm87 /homeassistant/components/pvoutput/ @frenck diff --git a/homeassistant/components/pushbullet/__init__.py b/homeassistant/components/pushbullet/__init__.py index 153fa389fcc726..bed0e94ccd930d 100644 --- a/homeassistant/components/pushbullet/__init__.py +++ b/homeassistant/components/pushbullet/__init__.py @@ -1 +1,79 @@ """The pushbullet component.""" +from __future__ import annotations + +import logging + +from pushbullet import InvalidKeyError, PushBullet, PushbulletError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + Platform, +) +from homeassistant.core import Event, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType + +from .api import PushBulletNotificationProvider +from .const import DATA_HASS_CONFIG, DOMAIN + +PLATFORMS = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the pushbullet component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up pushbullet from a config entry.""" + + try: + pushbullet = await hass.async_add_executor_job( + PushBullet, entry.data[CONF_API_KEY] + ) + except InvalidKeyError: + _LOGGER.error("Invalid API key for Pushbullet") + return False + except PushbulletError as err: + raise ConfigEntryNotReady from err + + pb_provider = PushBulletNotificationProvider(hass, pushbullet) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = pb_provider + + def start_listener(event: Event) -> None: + """Start the listener thread.""" + _LOGGER.debug("Starting listener for pushbullet") + pb_provider.start() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_listener) + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_NAME: entry.data[CONF_NAME], "entry_id": entry.entry_id}, + hass.data[DATA_HASS_CONFIG], + ) + ) + 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): + pb_provider: PushBulletNotificationProvider = hass.data[DOMAIN].pop( + entry.entry_id + ) + await hass.async_add_executor_job(pb_provider.close) + return unload_ok diff --git a/homeassistant/components/pushbullet/api.py b/homeassistant/components/pushbullet/api.py new file mode 100644 index 00000000000000..ff6a57aa9319d6 --- /dev/null +++ b/homeassistant/components/pushbullet/api.py @@ -0,0 +1,32 @@ +"""Pushbullet Notification provider.""" + +from typing import Any + +from pushbullet import Listener, PushBullet + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import DATA_UPDATED + + +class PushBulletNotificationProvider(Listener): + """Provider for an account, leading to one or more sensors.""" + + def __init__(self, hass: HomeAssistant, pushbullet: PushBullet) -> None: + """Start to retrieve pushes from the given Pushbullet instance.""" + self.hass = hass + self.pushbullet = pushbullet + self.data: dict[str, Any] = {} + super().__init__(account=pushbullet, on_push=self.update_data) + self.daemon = True + + def update_data(self, data: dict[str, Any]) -> None: + """Update the current data. + + Currently only monitors pushes but might be extended to monitor + different kinds of Pushbullet events. + """ + if data["type"] == "push": + self.data = data["push"] + dispatcher_send(self.hass, DATA_UPDATED) diff --git a/homeassistant/components/pushbullet/config_flow.py b/homeassistant/components/pushbullet/config_flow.py new file mode 100644 index 00000000000000..bfa12a911b6358 --- /dev/null +++ b/homeassistant/components/pushbullet/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for pushbullet integration.""" +from __future__ import annotations + +from typing import Any + +from pushbullet import InvalidKeyError, PushBullet, PushbulletError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector + +from .const import DEFAULT_NAME, DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): selector.TextSelector(), + vol.Required(CONF_API_KEY): selector.TextSelector(), + } +) + + +class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for pushbullet integration.""" + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Handle import from config.""" + import_config[CONF_NAME] = import_config.get(CONF_NAME, DEFAULT_NAME) + return await self.async_step_user(import_config) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + + self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) + + try: + pushbullet = await self.hass.async_add_executor_job( + PushBullet, user_input[CONF_API_KEY] + ) + except InvalidKeyError: + errors[CONF_API_KEY] = "invalid_api_key" + except PushbulletError: + errors["base"] = "cannot_connect" + + if not errors: + await self.async_set_unique_id(pushbullet.user_info["iden"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=CONFIG_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/pushbullet/const.py b/homeassistant/components/pushbullet/const.py new file mode 100644 index 00000000000000..de81f56e8624be --- /dev/null +++ b/homeassistant/components/pushbullet/const.py @@ -0,0 +1,12 @@ +"""Constants for the pushbullet integration.""" + +from typing import Final + +DOMAIN: Final = "pushbullet" +DEFAULT_NAME: Final = "Pushbullet" +DATA_HASS_CONFIG: Final = "pushbullet_hass_config" +DATA_UPDATED: Final = "pushbullet_data_updated" + +ATTR_URL: Final = "url" +ATTR_FILE: Final = "file" +ATTR_FILE_URL: Final = "file_url" diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 7931cca70ccf5d..7fcaa59fbb8e8a 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -3,7 +3,8 @@ "name": "Pushbullet", "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": ["pushbullet.py==0.11.0"], - "codeowners": [], + "codeowners": ["@engrbm87"], + "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pushbullet"] } diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 6f851f8000ee87..fcc9d00dc7a477 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -1,8 +1,13 @@ """Pushbullet platform for notify component.""" +from __future__ import annotations + import logging import mimetypes +from typing import Any -from pushbullet import InvalidKeyError, PushBullet, PushError +from pushbullet import PushBullet, PushError +from pushbullet.channel import Channel +from pushbullet.device import Device import voluptuous as vol from homeassistant.components.notify import ( @@ -13,59 +18,69 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -_LOGGER = logging.getLogger(__name__) +from .const import ATTR_FILE, ATTR_FILE_URL, ATTR_URL, DOMAIN -ATTR_URL = "url" -ATTR_FILE = "file" -ATTR_FILE_URL = "file_url" -ATTR_LIST = "list" +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string}) -def get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> PushBulletNotificationService | None: """Get the Pushbullet notification service.""" - - try: - pushbullet = PushBullet(config[CONF_API_KEY]) - except InvalidKeyError: - _LOGGER.error("Wrong API key supplied") + if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.2.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, + ) + ) return None - return PushBulletNotificationService(pushbullet) + pushbullet: PushBullet = hass.data[DOMAIN][discovery_info["entry_id"]].pushbullet + return PushBulletNotificationService(hass, pushbullet) class PushBulletNotificationService(BaseNotificationService): """Implement the notification service for Pushbullet.""" - def __init__(self, pb): # pylint: disable=invalid-name + def __init__(self, hass: HomeAssistant, pushbullet: PushBullet) -> None: """Initialize the service.""" - self.pushbullet = pb - self.pbtargets = {} - self.refresh() + self.hass = hass + self.pushbullet = pushbullet - def refresh(self): - """Refresh devices, contacts, etc. - - pbtargets stores all targets available from this Pushbullet instance - into a dict. These are Pushbullet objects!. It sacrifices a bit of - memory for faster processing at send_message. - - As of sept 2015, contacts were replaced by chats. This is not - implemented in the module yet. - """ - self.pushbullet.refresh() - self.pbtargets = { + @property + def pbtargets(self) -> dict[str, dict[str, Device | Channel]]: + """Return device and channel detected targets.""" + return { "device": {tgt.nickname.lower(): tgt for tgt in self.pushbullet.devices}, "channel": { tgt.channel_tag.lower(): tgt for tgt in self.pushbullet.channels }, } - def send_message(self, message=None, **kwargs): + def send_message(self, message: str, **kwargs: Any) -> None: """Send a message to a specified target. If no target specified, a 'normal' push will be sent to all devices @@ -73,24 +88,25 @@ def send_message(self, message=None, **kwargs): Email is special, these are assumed to always exist. We use a special call which doesn't require a push object. """ - targets = kwargs.get(ATTR_TARGET) - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - data = kwargs.get(ATTR_DATA) - refreshed = False + targets: list[str] = kwargs.get(ATTR_TARGET, []) + title: str = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + data: dict[str, Any] = kwargs[ATTR_DATA] or {} if not targets: # Backward compatibility, notify all devices in own account. self._push_data(message, title, data, self.pushbullet) - _LOGGER.info("Sent notification to self") + _LOGGER.debug("Sent notification to self") return + # refresh device and channel targets + self.pushbullet.refresh() + # Main loop, process all targets specified. for target in targets: try: ttype, tname = target.split("/", 1) - except ValueError: - _LOGGER.error("Invalid target syntax: %s", target) - continue + except ValueError as err: + raise ValueError(f"Invalid target syntax: '{target}'") from err # Target is email, send directly, don't use a target object. # This also seems to work to send to all devices in own account. @@ -107,71 +123,57 @@ def send_message(self, message=None, **kwargs): _LOGGER.info("Sent sms notification to %s", tname) continue - # Refresh if name not found. While awaiting periodic refresh - # solution in component, poor mans refresh. if ttype not in self.pbtargets: - _LOGGER.error("Invalid target syntax: %s", target) - continue + raise ValueError(f"Invalid target syntax: {target}") tname = tname.lower() - if tname not in self.pbtargets[ttype] and not refreshed: - self.refresh() - refreshed = True + if tname not in self.pbtargets[ttype]: + raise ValueError(f"Target: {target} doesn't exist") # Attempt push_note on a dict value. Keys are types & target # name. Dict pbtargets has all *actual* targets. - try: - self._push_data(message, title, data, self.pbtargets[ttype][tname]) - _LOGGER.info("Sent notification to %s/%s", ttype, tname) - except KeyError: - _LOGGER.error("No such target: %s/%s", ttype, tname) - continue - - def _push_data(self, message, title, data, pusher, email=None, phonenumber=None): + self._push_data(message, title, data, self.pbtargets[ttype][tname]) + _LOGGER.debug("Sent notification to %s/%s", ttype, tname) + + def _push_data( + self, + message: str, + title: str, + data: dict[str, Any], + pusher: PushBullet, + email: str | None = None, + phonenumber: str | None = None, + ): """Create the message content.""" + kwargs = {"body": message, "title": title} + if email: + kwargs["email"] = email - if data is None: - data = {} - data_list = data.get(ATTR_LIST) - url = data.get(ATTR_URL) - filepath = data.get(ATTR_FILE) - file_url = data.get(ATTR_FILE_URL) try: - email_kwargs = {} - if email: - email_kwargs["email"] = email - if phonenumber: - device = pusher.devices[0] - pusher.push_sms(device, phonenumber, message) - elif url: - pusher.push_link(title, url, body=message, **email_kwargs) - elif filepath: + if phonenumber and pusher.devices: + pusher.push_sms(pusher.devices[0], phonenumber, message) + return + if url := data.get(ATTR_URL): + pusher.push_link(url=url, **kwargs) + return + if filepath := data.get(ATTR_FILE): if not self.hass.config.is_allowed_path(filepath): - _LOGGER.error("Filepath is not valid or allowed") - return + raise ValueError("Filepath is not valid or allowed") with open(filepath, "rb") as fileh: filedata = self.pushbullet.upload_file(fileh, filepath) - if filedata.get("file_type") == "application/x-empty": - _LOGGER.error("Can not send an empty file") - return - filedata.update(email_kwargs) - pusher.push_file(title=title, body=message, **filedata) - elif file_url: - if not file_url.startswith("http"): - _LOGGER.error("URL should start with http or https") - return + if filedata.get("file_type") == "application/x-empty": + raise ValueError("Cannot send an empty file") + kwargs.update(filedata) + pusher.push_file(**kwargs) + elif (file_url := data.get(ATTR_FILE_URL)) and vol.Url(file_url): pusher.push_file( - title=title, - body=message, file_name=file_url, file_url=file_url, file_type=(mimetypes.guess_type(file_url)[0]), - **email_kwargs, + **kwargs, ) - elif data_list: - pusher.push_list(title, data_list, **email_kwargs) else: - pusher.push_note(title, message, **email_kwargs) + pusher.push_note(**kwargs) except PushError as err: - _LOGGER.error("Notify failed: %s", err) + raise HomeAssistantError(f"Notify failed: {err}") from err diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 51a18f1aaeab86..aef97991c66410 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -1,10 +1,6 @@ """Pushbullet platform for sensor component.""" from __future__ import annotations -import logging -import threading - -from pushbullet import InvalidKeyError, Listener, PushBullet import voluptuous as vol from homeassistant.components.sensor import ( @@ -12,18 +8,25 @@ SensorEntity, SensorEntityDescription, ) -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.dispatcher import async_dispatcher_connect +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__) +from .api import PushBulletNotificationProvider +from .const import DATA_UPDATED, DOMAIN SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="application_name", name="Application name", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="body", @@ -32,26 +35,32 @@ SensorEntityDescription( key="notification_id", name="Notification ID", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="notification_tag", name="Notification tag", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="package_name", name="Package name", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="receiver_email", name="Receiver email", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="sender_email", name="Sender email", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="source_device_iden", name="Sender device ID", + entity_registry_enabled_default=False, ), SensorEntityDescription( key="title", @@ -60,6 +69,7 @@ SensorEntityDescription( key="type", name="Type", + entity_registry_enabled_default=False, ), ) @@ -75,94 +85,88 @@ ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Pushbullet Sensor platform.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.2.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, + ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Pushbullet sensors from config entry.""" - try: - pushbullet = PushBullet(config.get(CONF_API_KEY)) - except InvalidKeyError: - _LOGGER.error("Wrong API key for Pushbullet supplied") - return - - pbprovider = PushBulletNotificationProvider(pushbullet) + pb_provider: PushBulletNotificationProvider = hass.data[DOMAIN][entry.entry_id] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] entities = [ - PushBulletNotificationSensor(pbprovider, description) + PushBulletNotificationSensor(entry.data[CONF_NAME], pb_provider, description) for description in SENSOR_TYPES - if description.key in monitored_conditions ] - add_entities(entities) + + async_add_entities(entities) class PushBulletNotificationSensor(SensorEntity): """Representation of a Pushbullet Sensor.""" + _attr_should_poll = False + _attr_has_entity_name = True + def __init__( self, - pb, # pylint: disable=invalid-name + name: str, + pb_provider: PushBulletNotificationProvider, description: SensorEntityDescription, - ): + ) -> None: """Initialize the Pushbullet sensor.""" self.entity_description = description - self.pushbullet = pb - - self._attr_name = f"Pushbullet {description.key}" - - def update(self) -> None: + self.pb_provider = pb_provider + self._attr_unique_id = ( + f"{pb_provider.pushbullet.user_info['iden']}-{description.key}" + ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, pb_provider.pushbullet.user_info["iden"])}, + name=name, + entry_type=DeviceEntryType.SERVICE, + ) + + @callback + def async_update_callback(self) -> None: """Fetch the latest data from the sensor. This will fetch the 'sensor reading' into self._state but also all attributes into self._state_attributes. """ try: - self._attr_native_value = self.pushbullet.data[self.entity_description.key] - self._attr_extra_state_attributes = self.pushbullet.data + self._attr_native_value = self.pb_provider.data[self.entity_description.key] + self._attr_extra_state_attributes = self.pb_provider.data except (KeyError, TypeError): pass - - -class PushBulletNotificationProvider: - """Provider for an account, leading to one or more sensors.""" - - def __init__(self, pushbullet): - """Start to retrieve pushes from the given Pushbullet instance.""" - - self.pushbullet = pushbullet - self._data = None - self.listener = None - self.thread = threading.Thread(target=self.retrieve_pushes) - self.thread.daemon = True - self.thread.start() - - def on_push(self, data): - """Update the current data. - - Currently only monitors pushes but might be extended to monitor - different kinds of Pushbullet events. - """ - if data["type"] == "push": - self._data = data["push"] - - @property - def data(self): - """Return the current data stored in the provider.""" - return self._data - - def retrieve_pushes(self): - """Retrieve_pushes. - - Spawn a new Listener and links it to self.on_push. - """ - - self.listener = Listener(account=self.pushbullet, on_push=self.on_push) - _LOGGER.debug("Getting pushes") - try: - self.listener.run_forever() - finally: - self.listener.close() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, DATA_UPDATED, self.async_update_callback + ) + ) diff --git a/homeassistant/components/pushbullet/strings.json b/homeassistant/components/pushbullet/strings.json new file mode 100644 index 00000000000000..92d22d117dcb10 --- /dev/null +++ b/homeassistant/components/pushbullet/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Pushbullet YAML configuration is being removed", + "description": "Configuring Pushbullet using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushbullet YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/pushbullet/translations/en.json b/homeassistant/components/pushbullet/translations/en.json new file mode 100644 index 00000000000000..97175ddf0b0c9f --- /dev/null +++ b/homeassistant/components/pushbullet/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "name": "Name" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Pushbullet using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushbullet YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Pushbullet 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 5279f34369127b..5214436be42ede 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -304,6 +304,7 @@ "prusalink", "ps4", "pure_energie", + "pushbullet", "pushover", "pvoutput", "pvpc_hourly_pricing", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 57452743af1e14..a955193624c785 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4085,7 +4085,7 @@ "pushbullet": { "name": "Pushbullet", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "pushover": { diff --git a/tests/components/pushbullet/__init__.py b/tests/components/pushbullet/__init__.py index c7f7911950c25a..74c89f33f2b73e 100644 --- a/tests/components/pushbullet/__init__.py +++ b/tests/components/pushbullet/__init__.py @@ -1 +1,5 @@ """Tests for the pushbullet component.""" + +from homeassistant.const import CONF_API_KEY, CONF_NAME + +MOCK_CONFIG = {CONF_NAME: "pushbullet", CONF_API_KEY: "MYAPIKEY"} diff --git a/tests/components/pushbullet/conftest.py b/tests/components/pushbullet/conftest.py new file mode 100644 index 00000000000000..5fadff3a825c49 --- /dev/null +++ b/tests/components/pushbullet/conftest.py @@ -0,0 +1,28 @@ +"""Conftest for pushbullet integration.""" + +from pushbullet import PushBullet +import pytest +from requests_mock import Mocker + +from tests.common import load_fixture + + +@pytest.fixture(autouse=True) +def requests_mock_fixture(requests_mock: Mocker) -> None: + """Fixture to provide a aioclient mocker.""" + requests_mock.get( + PushBullet.DEVICES_URL, + text=load_fixture("devices.json", "pushbullet"), + ) + requests_mock.get( + PushBullet.ME_URL, + text=load_fixture("user_info.json", "pushbullet"), + ) + requests_mock.get( + PushBullet.CHATS_URL, + text=load_fixture("chats.json", "pushbullet"), + ) + requests_mock.get( + PushBullet.CHANNELS_URL, + text=load_fixture("channels.json", "pushbullet"), + ) diff --git a/tests/components/pushbullet/fixtures/channels.json b/tests/components/pushbullet/fixtures/channels.json new file mode 100644 index 00000000000000..b95ca50b7c2944 --- /dev/null +++ b/tests/components/pushbullet/fixtures/channels.json @@ -0,0 +1,14 @@ +{ + "channels": [ + { + "active": true, + "created": 1412047948.579029, + "description": "Sample channel.", + "iden": "ujxPklLhvyKsjAvkMyTVh6", + "image_url": "https://dl.pushbulletusercontent.com/abc123/image.jpg", + "modified": 1412047948.579031, + "name": "Sample channel", + "tag": "sample-channel" + } + ] +} diff --git a/tests/components/pushbullet/fixtures/chats.json b/tests/components/pushbullet/fixtures/chats.json new file mode 100644 index 00000000000000..4c52bcc58cc5c1 --- /dev/null +++ b/tests/components/pushbullet/fixtures/chats.json @@ -0,0 +1,18 @@ +{ + "chats": [ + { + "active": true, + "created": 1412047948.579029, + "iden": "ujpah72o0sjAoRtnM0jc", + "modified": 1412047948.579031, + "with": { + "email": "someone@example.com", + "email_normalized": "someone@example.com", + "iden": "ujlMns72k", + "image_url": "https://dl.pushbulletusercontent.com/acb123/example.jpg", + "name": "Someone", + "type": "user" + } + } + ] +} diff --git a/tests/components/pushbullet/fixtures/user_info.json b/tests/components/pushbullet/fixtures/user_info.json new file mode 100644 index 00000000000000..3a17cccbf07f1e --- /dev/null +++ b/tests/components/pushbullet/fixtures/user_info.json @@ -0,0 +1,10 @@ +{ + "created": 1381092887.398433, + "email": "example@email.com", + "email_normalized": "example@email.com", + "iden": "ujpah72o0", + "image_url": "https://static.pushbullet.com/missing-image/55a7dc-45", + "max_upload_size": 26214400, + "modified": 1441054560.741007, + "name": "Some name" +} diff --git a/tests/components/pushbullet/test_config_flow.py b/tests/components/pushbullet/test_config_flow.py new file mode 100644 index 00000000000000..a19c424c8be879 --- /dev/null +++ b/tests/components/pushbullet/test_config_flow.py @@ -0,0 +1,134 @@ +"""Test pushbullet config flow.""" +from unittest.mock import patch + +from pushbullet import InvalidKeyError, PushbulletError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.pushbullet.const import DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def pushbullet_setup_fixture(): + """Patch pushbullet setup entry.""" + with patch( + "homeassistant.components.pushbullet.async_setup_entry", return_value=True + ): + yield + + +async def test_flow_user(hass: HomeAssistant, requests_mock_fixture) -> None: + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "pushbullet" + assert result["data"] == MOCK_CONFIG + + +async def test_flow_user_already_configured( + hass: HomeAssistant, requests_mock_fixture +) -> None: + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="ujpah72o0", + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_name_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="MYAPIKEY", + ) + + entry.add_to_hass(hass) + + new_config = MOCK_CONFIG.copy() + new_config[CONF_API_KEY] = "NEWKEY" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=new_config, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_invalid_key(hass: HomeAssistant) -> None: + """Test user initialized flow with invalid api key.""" + + with patch( + "homeassistant.components.pushbullet.config_flow.PushBullet", + side_effect=InvalidKeyError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_flow_conn_error(hass: HomeAssistant) -> None: + """Test user initialized flow with conn error.""" + + with patch( + "homeassistant.components.pushbullet.config_flow.PushBullet", + side_effect=PushbulletError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_import(hass: HomeAssistant, requests_mock_fixture) -> None: + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "pushbullet" + assert result["data"] == MOCK_CONFIG diff --git a/tests/components/pushbullet/test_init.py b/tests/components/pushbullet/test_init.py new file mode 100644 index 00000000000000..6f8e3776b35c2e --- /dev/null +++ b/tests/components/pushbullet/test_init.py @@ -0,0 +1,84 @@ +"""Test pushbullet integration.""" +from unittest.mock import patch + +from pushbullet import InvalidKeyError, PushbulletError + +from homeassistant.components.pushbullet.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import HomeAssistant + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +async def test_async_setup_entry_success( + hass: HomeAssistant, requests_mock_fixture +) -> None: + """Test pushbullet successful setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.pushbullet.api.PushBulletNotificationProvider.start" + ) as mock_start: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + mock_start.assert_called_once() + + +async def test_setup_entry_failed_invalid_key(hass: HomeAssistant) -> None: + """Test pushbullet failed setup due to invalid key.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.pushbullet.PushBullet", + side_effect=InvalidKeyError, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_setup_entry_failed_conn_error(hass: HomeAssistant) -> None: + """Test pushbullet failed setup due to conn error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.pushbullet.PushBullet", + side_effect=PushbulletError, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_async_unload_entry(hass: HomeAssistant, requests_mock_fixture) -> None: + """Test pushbullet unload entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index a9186652f6469c..2773679f22a50f 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -1,109 +1,65 @@ -"""The tests for the pushbullet notification platform.""" +"""Test pushbullet notification platform.""" from http import HTTPStatus -import json -from unittest.mock import patch -from pushbullet import PushBullet -import pytest +from requests_mock import Mocker -import homeassistant.components.notify as notify -from homeassistant.setup import async_setup_component +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.pushbullet.const import DOMAIN -from tests.common import assert_setup_component, load_fixture +from . import MOCK_CONFIG +from tests.common import MockConfigEntry -@pytest.fixture -def mock_pushbullet(): - """Mock pushbullet.""" - with patch.object( - PushBullet, - "_get_data", - return_value=json.loads(load_fixture("devices.json", "pushbullet")), - ): - yield - -async def test_pushbullet_config(hass, mock_pushbullet): - """Test setup.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] - - -async def test_pushbullet_config_bad(hass): - """Test set up the platform with bad/missing configuration.""" - config = {notify.DOMAIN: {"platform": "pushbullet"}} - with assert_setup_component(0) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert not handle_config[notify.DOMAIN] - - -async def test_pushbullet_push_default(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_default(hass, requests_mock: Mocker): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = {"title": "Test Title", "message": "Test Message"} - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 1 expected_body = {"body": "Test Message", "title": "Test Title", "type": "note"} + assert requests_mock.last_request assert requests_mock.last_request.json() == expected_body -async def test_pushbullet_push_device(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_device(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = { "title": "Test Title", "message": "Test Message", "target": ["device/DESKTOP"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 1 expected_body = { "body": "Test Message", @@ -114,35 +70,29 @@ async def test_pushbullet_push_device(hass, requests_mock, mock_pushbullet): assert requests_mock.last_request.json() == expected_body -async def test_pushbullet_push_devices(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_devices(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = { "title": "Test Title", "message": "Test Message", "target": ["device/DESKTOP", "device/My iPhone"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 2 - assert len(requests_mock.request_history) == 2 expected_body = { "body": "Test Message", @@ -150,45 +100,39 @@ async def test_pushbullet_push_devices(hass, requests_mock, mock_pushbullet): "title": "Test Title", "type": "note", } - assert requests_mock.request_history[0].json() == expected_body + assert requests_mock.request_history[-2].json() == expected_body expected_body = { "body": "Test Message", "device_iden": "identity2", "title": "Test Title", "type": "note", } - assert requests_mock.request_history[1].json() == expected_body + assert requests_mock.request_history[-1].json() == expected_body -async def test_pushbullet_push_email(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_email(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + data = { "title": "Test Title", "message": "Test Message", "target": ["email/user@host.net"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 1 - assert len(requests_mock.request_history) == 1 expected_body = { "body": "Test Message", @@ -196,38 +140,30 @@ async def test_pushbullet_push_email(hass, requests_mock, mock_pushbullet): "title": "Test Title", "type": "note", } - assert requests_mock.request_history[0].json() == expected_body + assert requests_mock.last_request.json() == expected_body -async def test_pushbullet_push_mixed(hass, requests_mock, mock_pushbullet): +async def test_pushbullet_push_mixed(hass, requests_mock): """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] requests_mock.register_uri( "POST", "https://api.pushbullet.com/v2/pushes", status_code=HTTPStatus.OK, json={"mock_response": "Ok"}, ) + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) data = { "title": "Test Title", "message": "Test Message", "target": ["device/DESKTOP", "email/user@host.net"], } - await hass.services.async_call(notify.DOMAIN, "test", data) + await hass.services.async_call(NOTIFY_DOMAIN, "pushbullet", data) await hass.async_block_till_done() - assert requests_mock.called - assert requests_mock.call_count == 2 - assert len(requests_mock.request_history) == 2 expected_body = { "body": "Test Message", @@ -235,40 +171,11 @@ async def test_pushbullet_push_mixed(hass, requests_mock, mock_pushbullet): "title": "Test Title", "type": "note", } - assert requests_mock.request_history[0].json() == expected_body + assert requests_mock.request_history[-2].json() == expected_body expected_body = { "body": "Test Message", "email": "user@host.net", "title": "Test Title", "type": "note", } - assert requests_mock.request_history[1].json() == expected_body - - -async def test_pushbullet_push_no_file(hass, requests_mock, mock_pushbullet): - """Test pushbullet push to default target.""" - config = { - notify.DOMAIN: { - "name": "test", - "platform": "pushbullet", - "api_key": "MYFAKEKEY", - } - } - with assert_setup_component(1) as handle_config: - assert await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert handle_config[notify.DOMAIN] - requests_mock.register_uri( - "POST", - "https://api.pushbullet.com/v2/pushes", - status_code=HTTPStatus.OK, - json={"mock_response": "Ok"}, - ) - data = { - "title": "Test Title", - "message": "Test Message", - "target": ["device/DESKTOP", "device/My iPhone"], - "data": {"file": "not_a_file"}, - } - assert not await hass.services.async_call(notify.DOMAIN, "test", data) - await hass.async_block_till_done() + assert requests_mock.request_history[-1].json() == expected_body From 442c5ccc0634a40ae2fa75eb51218ea68e3331bf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Nov 2022 16:25:18 +0100 Subject: [PATCH 183/394] Use attr in mqtt fan (#81401) * Use attr in mqtt fan * Fix is_on --- homeassistant/components/mqtt/fan.py | 88 +++++++++------------------- 1 file changed, 28 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 866b429c68f0b5..1b6c3e425a48e2 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -265,11 +265,8 @@ class MqttFan(MqttEntity, FanEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" - self._state = None - self._percentage = None - self._preset_mode = None - self._oscillation = None - self._supported_features = 0 + self._attr_percentage = None + self._attr_preset_mode = None self._topic = None self._payload = None @@ -330,11 +327,11 @@ def _setup_from_config(self, config): self._feature_percentage = CONF_PERCENTAGE_COMMAND_TOPIC in config self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: - self._preset_modes = config[CONF_PRESET_MODES_LIST] + self._attr_preset_modes = config[CONF_PRESET_MODES_LIST] else: - self._preset_modes = [] + self._attr_preset_modes = [] - self._speed_count = ( + self._attr_speed_count = ( min(int_states_in_range(self._speed_range), 100) if self._feature_percentage else 100 @@ -352,15 +349,15 @@ def _setup_from_config(self, config): optimistic or self._topic[CONF_PRESET_MODE_STATE_TOPIC] is None ) - self._supported_features = 0 - self._supported_features |= ( + self._attr_supported_features = 0 + self._attr_supported_features |= ( self._topic[CONF_OSCILLATION_COMMAND_TOPIC] is not None and FanEntityFeature.OSCILLATE ) if self._feature_percentage: - self._supported_features |= FanEntityFeature.SET_SPEED + self._attr_supported_features |= FanEntityFeature.SET_SPEED if self._feature_preset_mode: - self._supported_features |= FanEntityFeature.PRESET_MODE + self._attr_supported_features |= FanEntityFeature.PRESET_MODE for key, tpl in self._command_templates.items(): self._command_templates[key] = MqttCommandTemplate( @@ -386,11 +383,11 @@ def state_received(msg): _LOGGER.debug("Ignoring empty state from '%s'", msg.topic) return if payload == self._payload["STATE_ON"]: - self._state = True + self._attr_is_on = True elif payload == self._payload["STATE_OFF"]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: @@ -412,7 +409,7 @@ def percentage_received(msg): _LOGGER.debug("Ignoring empty speed from '%s'", msg.topic) return if rendered_percentage_payload == self._payload["PERCENTAGE_RESET"]: - self._percentage = None + self._attr_percentage = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return try: @@ -435,7 +432,7 @@ def percentage_received(msg): rendered_percentage_payload, ) return - self._percentage = percentage + self._attr_percentage = percentage get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PERCENTAGE_STATE_TOPIC] is not None: @@ -445,7 +442,7 @@ def percentage_received(msg): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._percentage = None + self._attr_percentage = None @callback @log_messages(self.hass, self.entity_id) @@ -453,7 +450,7 @@ def preset_mode_received(msg): """Handle new received MQTT message for preset mode.""" preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) if preset_mode == self._payload["PRESET_MODE_RESET"]: - self._preset_mode = None + self._attr_preset_mode = None self.async_write_ha_state() return if not preset_mode: @@ -468,7 +465,7 @@ def preset_mode_received(msg): ) return - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_PRESET_MODE_STATE_TOPIC] is not None: @@ -478,7 +475,7 @@ def preset_mode_received(msg): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._preset_mode = None + self._attr_preset_mode = None @callback @log_messages(self.hass, self.entity_id) @@ -489,9 +486,9 @@ def oscillation_received(msg): _LOGGER.debug("Ignoring empty oscillation from '%s'", msg.topic) return if payload == self._payload["OSCILLATE_ON_PAYLOAD"]: - self._oscillation = True + self._attr_oscillating = True elif payload == self._payload["OSCILLATE_OFF_PAYLOAD"]: - self._oscillation = False + self._attr_oscillating = False get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: @@ -501,7 +498,7 @@ def oscillation_received(msg): "qos": self._config[CONF_QOS], "encoding": self._config[CONF_ENCODING] or None, } - self._oscillation = False + self._attr_oscillating = False self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, self._sub_state, topics @@ -519,37 +516,8 @@ def assumed_state(self) -> bool: @property def is_on(self) -> bool | None: """Return true if device is on.""" - return self._state - - @property - def percentage(self) -> int | None: - """Return the current percentage.""" - return self._percentage - - @property - def preset_mode(self) -> str | None: - """Return the current preset _mode.""" - return self._preset_mode - - @property - def preset_modes(self) -> list[str]: - """Get the list of available preset modes.""" - return self._preset_modes - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return self._speed_count - - @property - def oscillating(self) -> bool | None: - """Return the oscillation state.""" - return self._oscillation + # The default for FanEntity is to compute it based on percentage + return self._attr_is_on # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7) async def async_turn_on( @@ -575,7 +543,7 @@ async def async_turn_on( if preset_mode: await self.async_set_preset_mode(preset_mode) if self._optimistic: - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -592,7 +560,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: self._config[CONF_ENCODING], ) if self._optimistic: - self._state = False + self._attr_is_on = False self.async_write_ha_state() async def async_set_percentage(self, percentage: int) -> None: @@ -613,7 +581,7 @@ async def async_set_percentage(self, percentage: int) -> None: ) if self._optimistic_percentage: - self._percentage = percentage + self._attr_percentage = percentage self.async_write_ha_state() async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -634,7 +602,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: ) if self._optimistic_preset_mode: - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode self.async_write_ha_state() async def async_oscillate(self, oscillating: bool) -> None: @@ -660,5 +628,5 @@ async def async_oscillate(self, oscillating: bool) -> None: ) if self._optimistic_oscillation: - self._oscillation = oscillating + self._attr_oscillating = oscillating self.async_write_ha_state() From 93d74cafdc1f6c34b164b6674e03cd98eda427f7 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 2 Nov 2022 17:52:36 +0100 Subject: [PATCH 184/394] Fix late review comments for Scrape (#81259) * Review comments from #74325 * Remove moved test * Fix init * Handle no data * Remove print * Fix tests * PlatformNotReady if no data * Recover failed platform setup * Fix broken test * patch context * reset test init * Move to platform * asyncio gather * Remove duplicate code --- homeassistant/components/scrape/__init__.py | 36 ++++++------- homeassistant/components/scrape/sensor.py | 55 ++++++++++---------- tests/components/scrape/test_init.py | 57 ++++++++++++++++++--- tests/components/scrape/test_sensor.py | 38 ++++++-------- 4 files changed, 113 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index 9b6fb9210f385a..25a734dbd24153 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1,8 +1,10 @@ """The scrape component.""" from __future__ import annotations +import asyncio +from collections.abc import Coroutine from datetime import timedelta -import logging +from typing import Any import voluptuous as vol @@ -15,7 +17,6 @@ Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA @@ -24,9 +25,6 @@ from .const import CONF_INDEX, CONF_SELECT, DEFAULT_SCAN_INTERVAL, DOMAIN from .coordinator import ScrapeCoordinator -_LOGGER = logging.getLogger(__name__) - - SENSOR_SCHEMA = vol.Schema( { **TEMPLATE_SENSOR_BASE_SCHEMA.schema, @@ -55,13 +53,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Scrape from yaml config.""" + scrape_config: list[ConfigType] | None if not (scrape_config := config.get(DOMAIN)): return True + load_coroutines: list[Coroutine[Any, Any, None]] = [] for resource_config in scrape_config: - if not (sensors := resource_config.get(SENSOR_DOMAIN)): - raise PlatformNotReady("No sensors configured") - rest = create_rest_data_from_config(hass, resource_config) coordinator = ScrapeCoordinator( hass, @@ -70,17 +67,20 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: seconds=resource_config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) ), ) - await coordinator.async_refresh() - if coordinator.data is None: - raise PlatformNotReady - for sensor_config in sensors: - discovery.load_platform( - hass, - Platform.SENSOR, - DOMAIN, - {"coordinator": coordinator, "config": sensor_config}, - config, + sensors: list[ConfigType] = resource_config.get(SENSOR_DOMAIN, []) + if sensors: + load_coroutines.append( + discovery.async_load_platform( + hass, + Platform.SENSOR, + DOMAIN, + {"coordinator": coordinator, "configs": sensors}, + config, + ) ) + if load_coroutines: + await asyncio.gather(*load_coroutines) + return True diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 176b556e1899c9..e911ade5d72746 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -83,6 +83,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" + entities: list[ScrapeSensor] = [] if discovery_info is None: async_create_issue( hass, @@ -97,45 +98,47 @@ async def async_setup_platform( rest = create_rest_data_from_config(hass, resource_config) coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL) - await coordinator.async_refresh() - if coordinator.data is None: - raise PlatformNotReady - sensor_config = config - template_config = vol.Schema( - TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA - )(sensor_config) + sensors_config: list[tuple[ConfigType, ConfigType]] = [ + ( + config, + vol.Schema(TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA)( + config + ), + ) + ] else: coordinator = discovery_info["coordinator"] - sensor_config = discovery_info["config"] - template_config = sensor_config + sensors_config = [ + (sensor_config, sensor_config) + for sensor_config in discovery_info["configs"] + ] - name: str = template_config[CONF_NAME] - unique_id: str | None = template_config.get(CONF_UNIQUE_ID) - select: str | None = sensor_config.get(CONF_SELECT) - attr: str | None = sensor_config.get(CONF_ATTRIBUTE) - index: int = sensor_config[CONF_INDEX] - value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) + await coordinator.async_refresh() + if coordinator.data is None: + raise PlatformNotReady - if value_template is not None: - value_template.hass = hass + for sensor_config, template_config in sensors_config: + value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass - async_add_entities( - [ + entities.append( ScrapeSensor( hass, coordinator, template_config, - name, - unique_id, - select, - attr, - index, + template_config[CONF_NAME], + template_config.get(CONF_UNIQUE_ID), + sensor_config.get(CONF_SELECT), + sensor_config.get(CONF_ATTRIBUTE), + sensor_config[CONF_INDEX], value_template, ) - ], - ) + ) + + async_add_entities(entities) class ScrapeSensor(CoordinatorEntity[ScrapeCoordinator], TemplateSensor): diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py index 22c07a2acaf957..e66b9a65fd450e 100644 --- a/tests/components/scrape/test_init.py +++ b/tests/components/scrape/test_init.py @@ -1,15 +1,21 @@ """Test Scrape component setup process.""" from __future__ import annotations +from datetime import datetime from unittest.mock import patch +import pytest + from homeassistant.components.scrape.const import DOMAIN +from homeassistant.components.scrape.sensor import SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from . import MockRestData, return_integration_config +from tests.common import async_fire_time_changed + async def test_setup_config(hass: HomeAssistant) -> None: """Test setup from yaml.""" @@ -35,8 +41,10 @@ async def test_setup_config(hass: HomeAssistant) -> None: assert len(mock_setup.mock_calls) == 1 -async def test_setup_no_data_fails(hass: HomeAssistant) -> None: - """Test setup entry no data fails.""" +async def test_setup_no_data_fails_with_recovery( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setup entry no data fails and recovers.""" config = { DOMAIN: [ return_integration_config( @@ -45,15 +53,25 @@ async def test_setup_no_data_fails(hass: HomeAssistant) -> None: ] } + mocker = MockRestData("test_scrape_sensor_no_data") with patch( - "homeassistant.components.scrape.coordinator.RestData", - return_value=MockRestData("test_scrape_sensor_no_data"), + "homeassistant.components.rest.RestData", + return_value=mocker, ): - assert not await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state is None + + assert "Platform scrape not ready yet" in caplog.text + + mocker.payload = "test_scrape_sensor" + async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") - assert state is None + assert state.state == "Current Version: 2021.12.10" async def test_setup_config_no_configuration(hass: HomeAssistant) -> None: @@ -65,3 +83,30 @@ async def test_setup_config_no_configuration(hass: HomeAssistant) -> None: entities = er.async_get(hass) assert entities.entities == {} + + +async def test_setup_config_no_sensors( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test setup from yaml with no configured sensors finalize properly.""" + config = { + DOMAIN: [ + { + "resource": "https://www.address.com", + "verify_ssl": True, + }, + { + "resource": "https://www.address2.com", + "verify_ssl": True, + "sensor": None, + }, + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.rest.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 24e583f5a873d2..2d0c7b4c61d173 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -4,6 +4,8 @@ from datetime import datetime from unittest.mock import patch +import pytest + from homeassistant.components.scrape.sensor import SCAN_INTERVAL from homeassistant.components.sensor import ( CONF_STATE_CLASS, @@ -67,6 +69,7 @@ async def test_scrape_sensor_platform_yaml(hass: HomeAssistant) -> None: name="Auth page2", username="user@secret.com", password="12345678", + template="{{value}}", ), ] } @@ -248,7 +251,9 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: assert state2.state == "secret text" -async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: +async def test_scrape_sensor_no_data( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test Scrape sensor fails on no data.""" config = { DOMAIN: return_integration_config( @@ -261,12 +266,14 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: "homeassistant.components.rest.RestData", return_value=mocker, ): - assert not await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is None + assert "Platform scrape not ready yet" in caplog.text + async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: """Test Scrape sensor no data on refresh.""" @@ -286,13 +293,13 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - state = hass.states.get("sensor.ha_version") - assert state - assert state.state == "Current Version: 2021.12.10" + state = hass.states.get("sensor.ha_version") + assert state + assert state.state == "Current Version: 2021.12.10" - mocker.payload = "test_scrape_sensor_no_data" - async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + mocker.payload = "test_scrape_sensor_no_data" + async_fire_time_changed(hass, datetime.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is not None @@ -398,18 +405,3 @@ async def test_scrape_sensor_unique_id(hass: HomeAssistant) -> None: entity = entity_reg.async_get("sensor.ha_version") assert entity.unique_id == "ha_version_unique_id" - - -async def test_scrape_sensor_not_configured_sensor(hass: HomeAssistant, caplog) -> None: - """Test Scrape sensor with missing configured sensors.""" - config = {DOMAIN: [return_integration_config(sensors=None)]} - - mocker = MockRestData("test_scrape_sensor") - with patch( - "homeassistant.components.rest.RestData", - return_value=mocker, - ): - assert not await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - assert "No sensors configured" in caplog.text From 7af0f16f79e1155723f095171b8796b227a9ebf1 Mon Sep 17 00:00:00 2001 From: Poltorak Serguei Date: Wed, 2 Nov 2022 19:53:10 +0300 Subject: [PATCH 185/394] Rework Z-Wave.Me to group entities of one physical devices (#78553) Co-authored-by: Martin Hjelmare Co-authored-by: Dmitry Vlasov --- homeassistant/components/zwave_me/__init__.py | 37 +++++++--- .../components/zwave_me/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../zwave_me/test_remove_stale_devices.py | 74 +++++++++++++++++++ 5 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 tests/components/zwave_me/test_remove_stale_devices.py diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index 10312f36dfc34c..ed3d538d052c86 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -8,6 +8,8 @@ from homeassistant.const import CONF_TOKEN, CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity @@ -23,6 +25,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: controller = hass.data[DOMAIN][entry.entry_id] = ZWaveMeController(hass, entry) if await controller.async_establish_connection(): hass.async_create_task(async_setup_platforms(hass, entry, controller)) + registry = device_registry.async_get(hass) + controller.remove_stale_devices(registry) return True raise ConfigEntryNotReady() @@ -62,24 +66,33 @@ async def async_establish_connection(self): def add_device(self, device: ZWaveMeData) -> None: """Send signal to create device.""" - if device.deviceType in ZWAVE_ME_PLATFORMS and self.platforms_inited: - if device.id in self.device_ids: - dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{device.id}", device) - else: - dispatcher_send( - self._hass, f"ZWAVE_ME_NEW_{device.deviceType.upper()}", device - ) - self.device_ids.add(device.id) - - def on_device_create(self, devices: list) -> None: + if device.id in self.device_ids: + dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{device.id}", device) + else: + dispatcher_send( + self._hass, f"ZWAVE_ME_NEW_{device.deviceType.upper()}", device + ) + self.device_ids.add(device.id) + + def on_device_create(self, devices: list[ZWaveMeData]) -> None: """Create multiple devices.""" for device in devices: - self.add_device(device) + if device.deviceType in ZWAVE_ME_PLATFORMS and self.platforms_inited: + self.add_device(device) def on_device_update(self, new_info: ZWaveMeData) -> None: """Send signal to update device.""" dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{new_info.id}", new_info) + def remove_stale_devices(self, registry: DeviceRegistry): + """Remove old-format devices in the registry.""" + for device_id in self.device_ids: + device = registry.async_get_device( + {(DOMAIN, f"{self.config.unique_id}-{device_id}")} + ) + if device is not None: + registry.async_remove_device(device.id) + async def async_setup_platforms( hass: HomeAssistant, entry: ConfigEntry, controller: ZWaveMeController @@ -113,7 +126,7 @@ def __init__(self, controller, device): def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, + identifiers={(DOMAIN, self.device.deviceIdentifier)}, name=self._attr_name, manufacturer=self.device.manufacturer, sw_version=self.device.firmware, diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 4ca933f43bc4b7..9aeeb7b2a40a1c 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave.Me", "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", - "requirements": ["zwave_me_ws==0.2.6", "url-normalize==1.4.3"], + "requirements": ["zwave_me_ws==0.3.0", "url-normalize==1.4.3"], "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index dd54335dd7101b..29e9d79aa0da4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2640,4 +2640,4 @@ zm-py==0.5.2 zwave-js-server-python==0.43.0 # homeassistant.components.zwave_me -zwave_me_ws==0.2.6 +zwave_me_ws==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ec5e21e4f693f..691b19b8b1235f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1832,4 +1832,4 @@ zigpy==0.51.5 zwave-js-server-python==0.43.0 # homeassistant.components.zwave_me -zwave_me_ws==0.2.6 +zwave_me_ws==0.3.0 diff --git a/tests/components/zwave_me/test_remove_stale_devices.py b/tests/components/zwave_me/test_remove_stale_devices.py new file mode 100644 index 00000000000000..484c38b9f33d18 --- /dev/null +++ b/tests/components/zwave_me/test_remove_stale_devices.py @@ -0,0 +1,74 @@ +"""Test the zwave_me removal of stale devices.""" +from unittest.mock import patch +import uuid + +import pytest as pytest +from zwave_me_ws import ZWaveMeData + +from homeassistant.components.zwave_me import ZWaveMePlatform +from homeassistant.const import CONF_TOKEN, CONF_URL +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, mock_device_registry + +DEFAULT_DEVICE_INFO = ZWaveMeData( + id="DummyDevice", + deviceType=ZWaveMePlatform.BINARY_SENSOR, + title="DeviceDevice", + level=100, + deviceIdentifier="16-23", +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +async def mock_connection(controller): + """Mock established connection and setting identifiers.""" + controller.on_new_device(DEFAULT_DEVICE_INFO) + return True + + +@pytest.mark.parametrize( + "identifier,should_exist", + [ + (DEFAULT_DEVICE_INFO.id, False), + (DEFAULT_DEVICE_INFO.deviceIdentifier, True), + ], +) +async def test_remove_stale_devices( + hass: HomeAssistant, device_reg, identifier, should_exist +): + """Test removing devices with old-format ids.""" + + config_entry = MockConfigEntry( + unique_id=uuid.uuid4(), + domain="zwave_me", + data={CONF_TOKEN: "test_token", CONF_URL: "http://test_test"}, + ) + config_entry.add_to_hass(hass) + device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={("mac", "12:34:56:AB:CD:EF")}, + identifiers={("zwave_me", f"{config_entry.unique_id}-{identifier}")}, + ) + with patch("zwave_me_ws.ZWaveMe.get_connection", mock_connection,), patch( + "homeassistant.components.zwave_me.async_setup_platforms", + ): + await hass.config_entries.async_setup(config_entry.entry_id) + assert ( + bool( + device_reg.async_get_device( + { + ( + "zwave_me", + f"{config_entry.unique_id}-{identifier}", + ) + } + ) + ) + == should_exist + ) From e1be63f26c6d24adb152196498fbbf0cfc948a7d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 2 Nov 2022 09:57:23 -0700 Subject: [PATCH 186/394] Bump gcal_sync to 2.2.3 (#81414) --- 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 9a184bdd636a2e..f6ebc665cd7506 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==2.2.2", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 29e9d79aa0da4a..1712fa513e6984 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -728,7 +728,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.2 +gcal-sync==2.2.3 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 691b19b8b1235f..a7e33c24d90cea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -544,7 +544,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.2 +gcal-sync==2.2.3 # homeassistant.components.geocaching geocachingapi==0.2.1 From 466365c8de4b4b519b89bcbc8efc67778d495d1f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:07:51 +0100 Subject: [PATCH 187/394] Fix Renault charging power sensor (#81412) --- homeassistant/components/renault/sensor.py | 12 ++++++++---- tests/components/renault/const.py | 13 ++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 3076dfc9f10621..436b9209e89249 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -23,7 +23,6 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, @@ -189,17 +188,22 @@ def _get_utc_value(entity: RenaultSensor[T]) -> datetime: state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( + # For vehicles that DO NOT report charging power in watts, this seems to + # correspond to the maximum power that would be admissible by the car based + # on the battery state, regardless of the type of charger. key="charging_power", condition_lambda=lambda a: not a.details.reports_charging_power_in_watts(), coordinator="battery", data_key="chargingInstantaneousPower", - device_class=SensorDeviceClass.CURRENT, + device_class=SensorDeviceClass.POWER, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging power", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + name="Admissible charging power", + native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, ), RenaultSensorEntityDescription( + # For vehicles that DO report charging power in watts, this is the power + # effectively being transferred to the car. key="charging_power", condition_lambda=lambda a: a.details.reports_charging_power_in_watts(), coordinator="battery", diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 785e27e1ea623a..97499d19bea635 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -27,7 +27,6 @@ ATTR_UNIT_OF_MEASUREMENT, CONF_PASSWORD, CONF_USERNAME, - ELECTRIC_CURRENT_AMPERE, ENERGY_KILO_WATT_HOUR, LENGTH_KILOMETERS, PERCENTAGE, @@ -398,12 +397,12 @@ ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_state", }, { - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_ENTITY_ID: "sensor.reg_number_charging_power", + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_ENTITY_ID: "sensor.reg_number_admissible_charging_power", ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -617,12 +616,12 @@ ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_state", }, { - ATTR_DEVICE_CLASS: SensorDeviceClass.CURRENT, - ATTR_ENTITY_ID: "sensor.reg_number_charging_power", + ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, + ATTR_ENTITY_ID: "sensor.reg_number_admissible_charging_power", ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_power", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_CURRENT_AMPERE, + ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", From d385a85ccbddaab9c4ba3754812b110f08b88b70 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:46:03 +0100 Subject: [PATCH 188/394] Cleanup schema validation in scrape sensor (#81419) --- homeassistant/components/scrape/sensor.py | 26 ++++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index e911ade5d72746..b606f00aaa7bfb 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -83,7 +83,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - entities: list[ScrapeSensor] = [] + coordinator: ScrapeCoordinator + sensors_config: list[ConfigType] if discovery_info is None: async_create_issue( hass, @@ -99,27 +100,22 @@ async def async_setup_platform( coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL) - sensors_config: list[tuple[ConfigType, ConfigType]] = [ - ( - config, - vol.Schema(TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.REMOVE_EXTRA)( - config - ), + sensors_config = [ + vol.Schema(TEMPLATE_SENSOR_BASE_SCHEMA.schema, extra=vol.ALLOW_EXTRA)( + config ) ] else: coordinator = discovery_info["coordinator"] - sensors_config = [ - (sensor_config, sensor_config) - for sensor_config in discovery_info["configs"] - ] + sensors_config = discovery_info["configs"] await coordinator.async_refresh() if coordinator.data is None: raise PlatformNotReady - for sensor_config, template_config in sensors_config: + entities: list[ScrapeSensor] = [] + for sensor_config in sensors_config: value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass @@ -128,9 +124,9 @@ async def async_setup_platform( ScrapeSensor( hass, coordinator, - template_config, - template_config[CONF_NAME], - template_config.get(CONF_UNIQUE_ID), + sensor_config, + sensor_config[CONF_NAME], + sensor_config.get(CONF_UNIQUE_ID), sensor_config.get(CONF_SELECT), sensor_config.get(CONF_ATTRIBUTE), sensor_config[CONF_INDEX], From 9afabc17ae112c37ea1e0a56953c03ec3bd40460 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 2 Nov 2022 19:50:11 +0100 Subject: [PATCH 189/394] Use attr in mqtt sensor (#81402) --- homeassistant/components/mqtt/sensor.py | 37 +++++++------------------ 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 52ba1a7e3c28e1..69077d30eee3e1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,7 +1,7 @@ """Support for MQTT sensors.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import functools import logging @@ -30,7 +30,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util from . import subscription @@ -171,7 +171,6 @@ class MqttSensor(MqttEntity, RestoreSensor): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the sensor.""" - self._state = None self._expiration_trigger = None expire_after = config.get(CONF_EXPIRE_AFTER) @@ -201,7 +200,7 @@ async def mqtt_async_added_to_hass(self) -> None: _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) return self._expired = False - self._state = last_sensor_data.native_value + self._attr_native_value = last_sensor_data.native_value self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at @@ -227,9 +226,13 @@ def config_schema(): """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_force_update = config[CONF_FORCE_UPDATE] + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_state_class = config.get(CONF_STATE_CLASS) + self._template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value @@ -259,7 +262,7 @@ def _update_state(msg): self.hass, self._value_is_expired, expiration_at ) - payload = self._template(msg.payload, default=self._state) + payload = self._template(msg.payload, default=self.native_value) if payload is not None and self.device_class in ( SensorDeviceClass.DATE, @@ -272,7 +275,7 @@ def _update_state(msg): elif self.device_class == SensorDeviceClass.DATE: payload = payload.date() - self._state = payload + self._attr_native_value = payload def _update_last_reset(msg): payload = self._last_reset_template(msg.payload) @@ -342,26 +345,6 @@ def _value_is_expired(self, *_): self._expired = True self.async_write_ha_state() - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return self._config.get(CONF_UNIT_OF_MEASUREMENT) - - @property - def native_value(self) -> StateType | datetime: - """Return the state of the entity.""" - return self._state - - @property - def device_class(self) -> str | None: - """Return the device class of the sensor.""" - return self._config.get(CONF_DEVICE_CLASS) - - @property - def state_class(self) -> str | None: - """Return the state class of the sensor.""" - return self._config.get(CONF_STATE_CLASS) - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" From 76819d81bec95e7776a34b420e8e457748690dcc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Nov 2022 20:25:31 +0100 Subject: [PATCH 190/394] Update frontend to 20221102.1 (#81422) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index be97ceee52292d..f4f46a1f89b304 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221102.0"], + "requirements": ["home-assistant-frontend==20221102.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5b5ee9c0e7b019..626e88420ea574 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 1712fa513e6984..a3a177edef5d2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -871,7 +871,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7e33c24d90cea..c8ceeb06fbd29f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -651,7 +651,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 # homeassistant.components.home_connect homeconnect==0.7.2 From 970fd9bdba5433ade6a92b8027663c8b27a5a5d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 2 Nov 2022 14:50:38 +0100 Subject: [PATCH 191/394] Update adax library to 0.1.5 (#81407) --- homeassistant/components/adax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index 408c099b8ac5ee..cbe14f0d7a5273 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -3,7 +3,7 @@ "name": "Adax", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", - "requirements": ["adax==0.2.0", "Adax-local==0.1.4"], + "requirements": ["adax==0.2.0", "Adax-local==0.1.5"], "codeowners": ["@danielhiversen"], "iot_class": "local_polling", "loggers": ["adax", "adax_local"] diff --git a/requirements_all.txt b/requirements_all.txt index 8ab3a60de07963..2cca9bdc50a85b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.2.1 AIOAladdinConnect==0.1.46 # homeassistant.components.adax -Adax-local==0.1.4 +Adax-local==0.1.5 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7be9395bbe3830..6cdc22b58c3c31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.2.1 AIOAladdinConnect==0.1.46 # homeassistant.components.adax -Adax-local==0.1.4 +Adax-local==0.1.5 # homeassistant.components.flick_electric PyFlick==0.0.2 From 28832e1c2e2b6473fd8dccebbb745256f860b923 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 2 Nov 2022 09:57:23 -0700 Subject: [PATCH 192/394] Bump gcal_sync to 2.2.3 (#81414) --- 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 9a184bdd636a2e..f6ebc665cd7506 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==2.2.2", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 2cca9bdc50a85b..8e941a1ed70e89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -725,7 +725,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.2 +gcal-sync==2.2.3 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cdc22b58c3c31..cb49c136842a08 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.2 +gcal-sync==2.2.3 # homeassistant.components.geocaching geocachingapi==0.2.1 From 1ea0d0e47f2a3b749f6158eb4b9f732e0b5c372c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 2 Nov 2022 20:25:31 +0100 Subject: [PATCH 193/394] Update frontend to 20221102.1 (#81422) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index be97ceee52292d..f4f46a1f89b304 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221102.0"], + "requirements": ["home-assistant-frontend==20221102.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 473b027edb7cd4..39158d63d550fd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 8e941a1ed70e89..bb2e4308e48e19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,7 +868,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb49c136842a08..a78ce61f213b46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,7 +648,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.0 +home-assistant-frontend==20221102.1 # homeassistant.components.home_connect homeconnect==0.7.2 From f14a84211f417d3e2d02b7e07e250f9acb716a86 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Nov 2022 20:29:00 +0100 Subject: [PATCH 194/394] Bumped version to 2022.11.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 532b8eee2ddf32..5ba07ebf8fdb76 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 11 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index c79195c95bd992..b41ac861aca3ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.11.0b7" +version = "2022.11.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b4ad03784f1d02995da39f3094c80adb4a60492b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 2 Nov 2022 20:33:18 +0100 Subject: [PATCH 195/394] Improve MQTT type hints part 1 (#80523) * Improve typing alarm_control_panel * Improve typing binary_sensor * Improve typing button * Add misssed annotation * Move CONF_EXPIRE_AFTER to _setup_from_config * Use CALL_BACK type * Remove assert, improve code style --- .../components/mqtt/alarm_control_panel.py | 68 +++++++++++-------- .../components/mqtt/binary_sensor.py | 61 ++++++++++------- homeassistant/components/mqtt/button.py | 20 ++++-- 3 files changed, 87 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index ed1990d919eab8..8a15c2587f162b 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -48,7 +48,7 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -155,8 +155,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) @@ -168,32 +168,39 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): _entity_id_format = alarm.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Init the MQTT Alarm Control Panel.""" self._state: str | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: + """(Re)Setup the entity.""" self._value_template = MqttValueTemplate( - self._config.get(CONF_VALUE_TEMPLATE), + config.get(CONF_VALUE_TEMPLATE), entity=self, ).async_render_with_possible_json_value self._command_template = MqttCommandTemplate( - self._config[CONF_COMMAND_TEMPLATE], entity=self + config[CONF_COMMAND_TEMPLATE], entity=self ).async_render - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Run when new MQTT message has been received.""" payload = self._value_template(msg.payload) if payload not in ( @@ -210,7 +217,7 @@ def message_received(msg): ): _LOGGER.warning("Received unexpected payload: %s", msg.payload) return - self._state = payload + self._state = str(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) self._sub_state = subscription.async_prepare_subscribe_topics( @@ -226,7 +233,7 @@ def message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -250,6 +257,7 @@ def supported_features(self) -> int: @property def code_format(self) -> alarm.CodeFormat | None: """Return one or more digits/characters.""" + code: str | None if (code := self._config.get(CONF_CODE)) is None: return None if code == REMOTE_CODE or (isinstance(code, str) and re.search("^\\d+$", code)): @@ -266,10 +274,10 @@ async def async_alarm_disarm(self, code: str | None = None) -> None: This method is a coroutine. """ - code_required = self._config[CONF_CODE_DISARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_DISARM_REQUIRED] if code_required and not self._validate_code(code, "disarming"): return - payload = self._config[CONF_PAYLOAD_DISARM] + payload: str = self._config[CONF_PAYLOAD_DISARM] await self._publish(code, payload) async def async_alarm_arm_home(self, code: str | None = None) -> None: @@ -277,10 +285,10 @@ async def async_alarm_arm_home(self, code: str | None = None) -> None: This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming home"): return - action = self._config[CONF_PAYLOAD_ARM_HOME] + action: str = self._config[CONF_PAYLOAD_ARM_HOME] await self._publish(code, action) async def async_alarm_arm_away(self, code: str | None = None) -> None: @@ -288,10 +296,10 @@ async def async_alarm_arm_away(self, code: str | None = None) -> None: This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming away"): return - action = self._config[CONF_PAYLOAD_ARM_AWAY] + action: str = self._config[CONF_PAYLOAD_ARM_AWAY] await self._publish(code, action) async def async_alarm_arm_night(self, code: str | None = None) -> None: @@ -299,10 +307,10 @@ async def async_alarm_arm_night(self, code: str | None = None) -> None: This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming night"): return - action = self._config[CONF_PAYLOAD_ARM_NIGHT] + action: str = self._config[CONF_PAYLOAD_ARM_NIGHT] await self._publish(code, action) async def async_alarm_arm_vacation(self, code: str | None = None) -> None: @@ -310,10 +318,10 @@ async def async_alarm_arm_vacation(self, code: str | None = None) -> None: This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming vacation"): return - action = self._config[CONF_PAYLOAD_ARM_VACATION] + action: str = self._config[CONF_PAYLOAD_ARM_VACATION] await self._publish(code, action) async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: @@ -321,10 +329,10 @@ async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: This method is a coroutine. """ - code_required = self._config[CONF_CODE_ARM_REQUIRED] + code_required: bool = self._config[CONF_CODE_ARM_REQUIRED] if code_required and not self._validate_code(code, "arming custom bypass"): return - action = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS] + action: str = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS] await self._publish(code, action) async def async_alarm_trigger(self, code: str | None = None) -> None: @@ -332,13 +340,13 @@ async def async_alarm_trigger(self, code: str | None = None) -> None: This method is a coroutine. """ - code_required = self._config[CONF_CODE_TRIGGER_REQUIRED] + code_required: bool = self._config[CONF_CODE_TRIGGER_REQUIRED] if code_required and not self._validate_code(code, "triggering"): return - action = self._config[CONF_PAYLOAD_TRIGGER] + action: str = self._config[CONF_PAYLOAD_TRIGGER] await self._publish(code, action) - async def _publish(self, code, action): + async def _publish(self, code: str | None, action: str) -> None: """Publish via mqtt.""" variables = {"action": action, "code": code} payload = self._command_template(None, variables=variables) @@ -350,10 +358,10 @@ async def _publish(self, code, action): self._config[CONF_ENCODING], ) - def _validate_code(self, code, state): + def _validate_code(self, code: str | None, state: str) -> bool: """Validate given code.""" - conf_code = self._config.get(CONF_CODE) - check = ( + conf_code: str | None = self._config.get(CONF_CODE) + check = bool( conf_code is None or code == conf_code or (conf_code == REMOTE_CODE and code) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index d9351908665df9..82818c5b7069b9 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,9 +1,10 @@ """Support for MQTT binary sensors.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import functools import logging +from typing import Any import voluptuous as vol @@ -24,7 +25,7 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.event as evt @@ -45,7 +46,7 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -111,8 +112,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT binary sensor.""" async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) @@ -122,16 +123,18 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Representation a binary sensor that is updated by MQTT.""" _entity_id_format = binary_sensor.ENTITY_ID_FORMAT - - def __init__(self, hass, config, config_entry, discovery_data): + _expired: bool | None + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT binary sensor.""" - self._expiration_trigger = None - self._delay_listener = None - expire_after = config.get(CONF_EXPIRE_AFTER) - if expire_after is not None and expire_after > 0: - self._expired = True - else: - self._expired = None + self._expiration_trigger: CALLBACK_TYPE | None = None + self._delay_listener: CALLBACK_TYPE | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -146,7 +149,9 @@ async def mqtt_async_added_to_hass(self) -> None: # MqttEntity.async_added_to_hass(), then we should not restore state and not self._expiration_trigger ): - expiration_at = last_state.last_changed + timedelta(seconds=expire_after) + expiration_at: datetime = last_state.last_changed + timedelta( + seconds=expire_after + ) if expiration_at < (time_now := dt_util.utcnow()): # Skip reactivating the binary_sensor _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) @@ -174,24 +179,30 @@ async def async_will_remove_from_hass(self) -> None: await MqttEntity.async_will_remove_from_hass(self) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA def _setup_from_config(self, config: ConfigType) -> None: - self._attr_device_class = config.get(CONF_DEVICE_CLASS) + """(Re)Setup the entity.""" + expire_after: int | None = config.get(CONF_EXPIRE_AFTER) + if expire_after is not None and expire_after > 0: + self._expired = True + else: + self._expired = None self._attr_force_update = config[CONF_FORCE_UPDATE] + self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._value_template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback - def off_delay_listener(now): + def off_delay_listener(now: datetime) -> None: """Switch device off after a delay.""" self._delay_listener = None self._attr_is_on = False @@ -199,10 +210,10 @@ def off_delay_listener(now): @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle a new received MQTT state message.""" # auto-expire enabled? - expire_after = self._config.get(CONF_EXPIRE_AFTER) + expire_after: int | None = self._config.get(CONF_EXPIRE_AFTER) if expire_after is not None and expire_after > 0: @@ -241,7 +252,7 @@ def state_message_received(msg): else: # Payload is not for this entity template_info = "" if self._config.get(CONF_VALUE_TEMPLATE) is not None: - template_info = f", template output: '{payload}', with value template '{str(self._config.get(CONF_VALUE_TEMPLATE))}'" + template_info = f", template output: '{str(payload)}', with value template '{str(self._config.get(CONF_VALUE_TEMPLATE))}'" _LOGGER.info( "No matching payload found for entity: %s with state topic: %s. Payload: '%s'%s", self._config[CONF_NAME], @@ -276,12 +287,12 @@ def state_message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @callback - def _value_is_expired(self, *_): + def _value_is_expired(self, *_: Any) -> None: """Triggered when value is expired.""" self._expiration_trigger = None self._expired = True @@ -291,7 +302,7 @@ def _value_is_expired(self, *_): @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" - expire_after = self._config.get(CONF_EXPIRE_AFTER) + expire_after: int | None = self._config.get(CONF_EXPIRE_AFTER) # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 return MqttAvailability.available.fget(self) and ( # type: ignore[attr-defined] expire_after is None or not self._expired diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index a14bf87c3be39f..d2743560411746 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -90,8 +90,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT button.""" async_add_entities([MqttButton(hass, config, config_entry, discovery_data)]) @@ -102,25 +102,31 @@ class MqttButton(MqttEntity, ButtonEntity): _entity_id_format = button.ENTITY_ID_FORMAT - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT button.""" MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._command_template = MqttCommandTemplate( config.get(CONF_COMMAND_TEMPLATE), entity=self ).async_render - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @property From bda7e416c441022eefabd18433debe7a80320dd3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 2 Nov 2022 20:33:46 +0100 Subject: [PATCH 196/394] Improve MQTT type hints part 2 (#80529) * Improve typing camera * Improve typing cover * b64 encoding can be either bytes or a string. --- homeassistant/components/mqtt/camera.py | 28 +++++--- homeassistant/components/mqtt/cover.py | 86 ++++++++++++++----------- 2 files changed, 67 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index f6039251882daa..5044f5abfaf5a7 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -27,6 +27,7 @@ async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import ReceiveMessage from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -114,8 +115,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Camera.""" async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)]) @@ -124,31 +125,38 @@ async def _async_setup_entity( class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" - _entity_id_format = camera.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_CAMERA_ATTRIBUTES_BLOCKED + _entity_id_format: str = camera.ENTITY_ID_FORMAT + _attributes_extra_blocked: frozenset[str] = MQTT_CAMERA_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT Camera.""" - self._last_image = None + self._last_image: bytes | None = None Camera.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" if CONF_IMAGE_ENCODING in self._config: self._last_image = b64decode(msg.payload) else: + assert isinstance(msg.payload, bytes) self._last_image = msg.payload self._sub_state = subscription.async_prepare_subscribe_topics( @@ -164,7 +172,7 @@ def message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 7d7d4f61c4a0f4..ef8b00900159d1 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -31,6 +31,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -50,7 +51,7 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -113,44 +114,44 @@ ) -def validate_options(value): +def validate_options(config: ConfigType) -> ConfigType: """Validate options. If set position topic is set then get position topic is set as well. """ - if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value: + if CONF_SET_POSITION_TOPIC in config and CONF_GET_POSITION_TOPIC not in config: raise vol.Invalid( f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) # if templates are set make sure the topic for the template is also set - if CONF_VALUE_TEMPLATE in value and CONF_STATE_TOPIC not in value: + if CONF_VALUE_TEMPLATE in config and CONF_STATE_TOPIC not in config: raise vol.Invalid( f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) - if CONF_GET_POSITION_TEMPLATE in value and CONF_GET_POSITION_TOPIC not in value: + if CONF_GET_POSITION_TEMPLATE in config and CONF_GET_POSITION_TOPIC not in config: raise vol.Invalid( f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) - if CONF_SET_POSITION_TEMPLATE in value and CONF_SET_POSITION_TOPIC not in value: + if CONF_SET_POSITION_TEMPLATE in config and CONF_SET_POSITION_TOPIC not in config: raise vol.Invalid( f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." ) - if CONF_TILT_COMMAND_TEMPLATE in value and CONF_TILT_COMMAND_TOPIC not in value: + if CONF_TILT_COMMAND_TEMPLATE in config and CONF_TILT_COMMAND_TOPIC not in config: raise vol.Invalid( f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." ) - if CONF_TILT_STATUS_TEMPLATE in value and CONF_TILT_STATUS_TOPIC not in value: + if CONF_TILT_STATUS_TEMPLATE in config and CONF_TILT_STATUS_TOPIC not in config: raise vol.Invalid( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." ) - return value + return config _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( @@ -251,8 +252,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Cover.""" async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) @@ -261,26 +262,32 @@ async def _async_setup_entity( class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" - _entity_id_format = cover.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_COVER_ATTRIBUTES_BLOCKED + _entity_id_format: str = cover.ENTITY_ID_FORMAT + _attributes_extra_blocked: frozenset[str] = MQTT_COVER_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the cover.""" - self._position = None - self._state = None + self._position: int | None = None + self._state: str | None = None - self._optimistic = None - self._tilt_value = None - self._tilt_optimistic = None + self._optimistic: bool | None = None + self._tilt_value: int | None = None + self._tilt_optimistic: bool | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: no_position = ( config.get(CONF_SET_POSITION_TOPIC) is None and config.get(CONF_GET_POSITION_TOPIC) is None @@ -353,13 +360,13 @@ def _setup_from_config(self, config): config_attributes=template_config_attributes, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" topics = {} @callback @log_messages(self.hass, self.entity_id) - def tilt_message_received(msg): + def tilt_message_received(msg: ReceiveMessage) -> None: """Handle tilt updates.""" payload = self._tilt_status_template(msg.payload) @@ -371,7 +378,7 @@ def tilt_message_received(msg): @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT state messages.""" payload = self._value_template(msg.payload) @@ -409,31 +416,32 @@ def state_message_received(msg): @callback @log_messages(self.hass, self.entity_id) - def position_message_received(msg): + def position_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT position messages.""" - payload = self._get_position_template(msg.payload) + payload: ReceivePayloadType = self._get_position_template(msg.payload) + payload_dict: Any = None if not payload: _LOGGER.debug("Ignoring empty position message from '%s'", msg.topic) return try: - payload = json_loads(payload) + payload_dict = json_loads(payload) except JSON_DECODE_EXCEPTIONS: pass - if isinstance(payload, dict): - if "position" not in payload: + if payload_dict and isinstance(payload_dict, dict): + if "position" not in payload_dict: _LOGGER.warning( "Template (position_template) returned JSON without position attribute" ) return - if "tilt_position" in payload: + if "tilt_position" in payload_dict: if not self._config.get(CONF_TILT_STATE_OPTIMISTIC): # reset forced set tilt optimistic self._tilt_optimistic = False - self.tilt_payload_received(payload["tilt_position"]) - payload = payload["position"] + self.tilt_payload_received(payload_dict["tilt_position"]) + payload = payload_dict["position"] try: percentage_payload = self.find_percentage_in_range( @@ -481,7 +489,7 @@ def position_message_received(msg): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -719,13 +727,15 @@ async def async_toggle_tilt(self, **kwargs: Any) -> None: else: await self.async_close_cover_tilt(**kwargs) - def is_tilt_closed(self): + def is_tilt_closed(self) -> bool: """Return if the cover is tilted closed.""" return self._tilt_value == self.find_percentage_in_range( float(self._config[CONF_TILT_CLOSED_POSITION]) ) - def find_percentage_in_range(self, position, range_type=TILT_PAYLOAD): + def find_percentage_in_range( + self, position: float, range_type: str = TILT_PAYLOAD + ) -> int: """Find the 0-100% value within the specified range.""" # the range of motion as defined by the min max values if range_type == COVER_PAYLOAD: @@ -745,7 +755,9 @@ def find_percentage_in_range(self, position, range_type=TILT_PAYLOAD): return position_percentage - def find_in_range_from_percent(self, percentage, range_type=TILT_PAYLOAD): + def find_in_range_from_percent( + self, percentage: float, range_type: str = TILT_PAYLOAD + ) -> int: """ Find the adjusted value for 0-100% within the specified range. @@ -768,7 +780,7 @@ def find_in_range_from_percent(self, percentage, range_type=TILT_PAYLOAD): return position @callback - def tilt_payload_received(self, _payload): + def tilt_payload_received(self, _payload: Any) -> None: """Set the tilt value.""" try: From 1beab9694605ae9a9b55dfd1a812c81449bb200b Mon Sep 17 00:00:00 2001 From: rappenze Date: Wed, 2 Nov 2022 23:02:44 +0100 Subject: [PATCH 197/394] Replace deprecated unit constants in fibaro sensor (#81425) --- homeassistant/components/fibaro/sensor.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 797fc6d8d443a6..da1b66c65287ed 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -14,13 +14,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, - ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, - POWER_WATT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, Platform, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -67,7 +66,7 @@ "com.fibaro.energyMeter": SensorEntityDescription( key="com.fibaro.energyMeter", name="Energy", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), @@ -79,14 +78,14 @@ SensorEntityDescription( key="energy", name="Energy", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="power", name="Power", - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), @@ -94,8 +93,8 @@ FIBARO_TO_HASS_UNIT: dict[str, str] = { "lux": LIGHT_LUX, - "C": TEMP_CELSIUS, - "F": TEMP_FAHRENHEIT, + "C": UnitOfTemperature.CELSIUS, + "F": UnitOfTemperature.FAHRENHEIT, } From 7995f0e414a04966c29a9854f86a427164118ed7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 3 Nov 2022 00:28:45 +0000 Subject: [PATCH 198/394] [ci skip] Translation update --- .../components/aranet/translations/id.json | 24 ++++ .../fireservicerota/translations/id.json | 6 + .../components/generic/translations/id.json | 7 ++ .../google_travel_time/translations/id.json | 4 +- .../components/hassio/translations/bg.json | 42 +++++++ .../components/hassio/translations/ca.json | 36 ++++++ .../components/hassio/translations/de.json | 104 ++++++++++++++++- .../components/hassio/translations/en.json | 2 +- .../components/hassio/translations/es.json | 104 ++++++++++++++++- .../components/hassio/translations/et.json | 104 ++++++++++++++++- .../components/hassio/translations/hu.json | 104 ++++++++++++++++- .../components/hassio/translations/id.json | 110 ++++++++++++++++++ .../components/hassio/translations/it.json | 104 ++++++++++++++++- .../components/hassio/translations/no.json | 104 ++++++++++++++++- .../components/hassio/translations/pt-BR.json | 104 ++++++++++++++++- .../components/hassio/translations/ru.json | 108 ++++++++++++++++- .../hassio/translations/zh-Hant.json | 104 ++++++++++++++++- .../components/mqtt/translations/id.json | 34 +++++- .../pushbullet/translations/ca.json | 25 ++++ .../pushbullet/translations/de.json | 25 ++++ .../pushbullet/translations/es.json | 25 ++++ .../pushbullet/translations/hu.json | 25 ++++ .../pushbullet/translations/it.json | 25 ++++ .../pushbullet/translations/pt-BR.json | 25 ++++ .../pushbullet/translations/ru.json | 25 ++++ 25 files changed, 1356 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/aranet/translations/id.json create mode 100644 homeassistant/components/pushbullet/translations/ca.json create mode 100644 homeassistant/components/pushbullet/translations/de.json create mode 100644 homeassistant/components/pushbullet/translations/es.json create mode 100644 homeassistant/components/pushbullet/translations/hu.json create mode 100644 homeassistant/components/pushbullet/translations/it.json create mode 100644 homeassistant/components/pushbullet/translations/pt-BR.json create mode 100644 homeassistant/components/pushbullet/translations/ru.json diff --git a/homeassistant/components/aranet/translations/id.json b/homeassistant/components/aranet/translations/id.json new file mode 100644 index 00000000000000..04e65296a12c3f --- /dev/null +++ b/homeassistant/components/aranet/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_devices_found": "Tidak ditemukan perangkat Aranet yang tidak dikonfigurasi.", + "outdated_version": "Perangkat ini menggunakan firmware usang. Perbarui setidaknya ke firmware v1.2.0 dan coba lagi." + }, + "error": { + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/id.json b/homeassistant/components/fireservicerota/translations/id.json index 0c4462a1ea7636..c7a5c894f62610 100644 --- a/homeassistant/components/fireservicerota/translations/id.json +++ b/homeassistant/components/fireservicerota/translations/id.json @@ -17,6 +17,12 @@ }, "description": "Token autentikasi menjadi tidak valid, masuk untuk membuat token lagi." }, + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Token autentikasi menjadi tidak valid, masuk untuk membuat token lagi." + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 8cc0ca6aefcac7..f379631074437a 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -75,6 +75,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "confirm_still": { + "data": { + "confirmed_ok": "Gambar ini terlihat bagus." + }, + "description": "![Pratinjau Gambar Diam Kamera]({preview_url})", + "title": "Pratinjau" + }, "content_type": { "data": { "content_type": "Jenis Konten" diff --git a/homeassistant/components/google_travel_time/translations/id.json b/homeassistant/components/google_travel_time/translations/id.json index e960d03c3410ad..bcfbafae4b7876 100644 --- a/homeassistant/components/google_travel_time/translations/id.json +++ b/homeassistant/components/google_travel_time/translations/id.json @@ -4,7 +4,8 @@ "already_configured": "Lokasi sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" }, "step": { "user": { @@ -27,6 +28,7 @@ "mode": "Mode Perjalanan", "time": "Waktu", "time_type": "Jenis Waktu", + "traffic_mode": "Mode Lalu Lintas", "transit_mode": "Mode Transit", "transit_routing_preference": "Preferensi Perutean Transit", "units": "Unit" diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index 451dd51d1257dd..cd7aa5c19233af 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -2,6 +2,48 @@ "issues": { "unsupported": { "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + }, + "unsupported_apparmor": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 AppArmor" + }, + "unsupported_cgroup_version": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 CGroup" + }, + "unsupported_dbus": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 D-Bus" + }, + "unsupported_dns_server": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 DNS \u0441\u044a\u0440\u0432\u044a\u0440\u0430" + }, + "unsupported_docker_version": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Docker" + }, + "unsupported_job_conditions": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0417\u0430\u0449\u0438\u0442\u0438\u0442\u0435 \u0441\u0430 \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0438" + }, + "unsupported_network_manager": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 Network Manager" + }, + "unsupported_os": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u041e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430" + }, + "unsupported_os_agent": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 OS-Agent" + }, + "unsupported_software": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d \u0441\u043e\u0444\u0442\u0443\u0435\u0440" + }, + "unsupported_supervisor_version": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Supervisor" + }, + "unsupported_systemd": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441\u044a\u0441 Systemd" + }, + "unsupported_systemd_journal": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441\u044a\u0441 Systemd Journal" + }, + "unsupported_systemd_resolved": { + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441\u044a\u0441 Systemd-Resolved" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 14679301993a1f..40a1c9435aae07 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -4,9 +4,45 @@ "description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.", "title": "Sistema no saludable - {reason}" }, + "unhealthy_docker": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 Docker no est\u00e0 configurat correctament. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Docker mal configurat" + }, + "unhealthy_privileged": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 no t\u00e9 acc\u00e9s privilegiat a l'execuci\u00f3 de Docker. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Sense privilegis" + }, + "unhealthy_setup": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 no s'ha pogut completar la configuraci\u00f3 correctament. Pot ser degut a diferents motius, clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Configuraci\u00f3 fallida" + }, + "unhealthy_supervisor": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 ha fallat un intent d'actualitzaci\u00f3 del Supervisor a l'\u00faltima versi\u00f3. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Actualitzaci\u00f3 del Supervisor fallida" + }, + "unhealthy_untrusted": { + "description": "El sistema no \u00e9s saludable perqu\u00e8 s'ha detectat codi o imatges no fiables. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no saludable - Codi no fiable" + }, "unsupported": { "description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.", "title": "Sistema no compatible - {reason}" + }, + "unsupported_apparmor": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 AppArmor no est\u00e0 funcionant correctament i els complements s'executen en un entorn insegur i no protegit. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb AppArmor" + }, + "unsupported_cgroup_version": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 de Docker CGroup incorrecta. Clica l'enlla\u00e7 per con\u00e8ixer la versi\u00f3 correcta i sobre com solucionar-ho.", + "title": "Sistema no compatible - Versi\u00f3 de CGroup" + }, + "unsupported_docker_configuration": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el 'deamon' de Docker est\u00e0 funcionant de manera inesperada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Docker mal configurat" + }, + "unsupported_docker_version": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 de Docker incorrecta. Clica l'enlla\u00e7 per con\u00e8ixer la versi\u00f3 correcta i sobre com solucionar-ho.", + "title": "Sistema no compatible - Versi\u00f3 de Docker" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index 54940b93448044..325009e4814611 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "Das System ist derzeit aufgrund von \u201e{reason}\u201c fehlerhaft. Verwende den Link, um mehr dar\u00fcber zu erfahren, was falsch ist und wie du es beheben kannst.", + "description": "Das System ist derzeit aufgrund von \u201e{reason}\u201c fehlerhaft. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", "title": "Fehlerhaftes System - {reason}" }, + "unhealthy_docker": { + "description": "Das System ist derzeit fehlerhaft, da Docker falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Ungesundes System - Docker falsch konfiguriert" + }, + "unhealthy_privileged": { + "description": "Das System ist derzeit fehlerhaft, da es keinen privilegierten Zugriff auf die Docker-Laufzeit hat. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Nicht privilegiert" + }, + "unhealthy_setup": { + "description": "Das System ist derzeit fehlerhaft, da die Einrichtung nicht abgeschlossen werden konnte. Dies kann mehrere Gr\u00fcnde haben. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Setup fehlgeschlagen" + }, + "unhealthy_supervisor": { + "description": "Das System ist derzeit fehlerhaft, weil ein Versuch, Supervisor auf die neueste Version zu aktualisieren, fehlgeschlagen ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Supervisor-Update fehlgeschlagen" + }, + "unhealthy_untrusted": { + "description": "Das System ist derzeit nicht fehlerfrei, da es nicht vertrauensw\u00fcrdigen Code oder verwendete Images erkannt hat. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Fehlerhaftes System \u2013 Nicht vertrauensw\u00fcrdiger Code" + }, "unsupported": { - "description": "Das System wird aufgrund von \u201e{reason}\u201c nicht unterst\u00fctzt. Verwende den Link, um mehr dar\u00fcber zu erfahren, was dies bedeutet und wie du zu einem unterst\u00fctzten System zur\u00fcckkehren kannst.", + "description": "Das System wird aufgrund von \u201e{reason}\u201c nicht unterst\u00fctzt. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", "title": "Nicht unterst\u00fctztes System \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "Das System wird nicht unterst\u00fctzt, da AppArmor nicht ordnungsgem\u00e4\u00df funktioniert und Add-Ons ungesch\u00fctzt und unsicher ausgef\u00fchrt werden. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - AppArmor-Probleme" + }, + "unsupported_cgroup_version": { + "description": "Das System wird nicht unterst\u00fctzt, da die falsche Version von Docker CGroup verwendet wird. Verwende den Link, um die richtige Version zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 CGroup-Version" + }, + "unsupported_connectivity_check": { + "description": "Das System wird nicht unterst\u00fctzt, weil Home Assistant nicht feststellen kann, wann eine Internetverbindung verf\u00fcgbar ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Konnektivit\u00e4tspr\u00fcfung deaktiviert" + }, + "unsupported_content_trust": { + "description": "Das System wird nicht unterst\u00fctzt, da Home Assistant nicht \u00fcberpr\u00fcfen kann, ob der ausgef\u00fchrte Inhalt vertrauensw\u00fcrdig ist und nicht von Angreifern ge\u00e4ndert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Inhaltsvertrauenspr\u00fcfung deaktiviert" + }, + "unsupported_dbus": { + "description": "System wird nicht unterst\u00fctzt, da D-Bus nicht richtig funktioniert. Viele Dinge schlagen ohne dies fehl, da Supervisor nicht mit dem Host kommunizieren kann. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 D-Bus-Probleme" + }, + "unsupported_dns_server": { + "description": "Das System wird nicht unterst\u00fctzt, da der bereitgestellte DNS-Server nicht ordnungsgem\u00e4\u00df funktioniert und die Fallback-DNS-Option deaktiviert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - DNS-Server-Probleme" + }, + "unsupported_docker_configuration": { + "description": "Das System wird nicht unterst\u00fctzt, da der Docker-Daemon auf unerwartete Weise ausgef\u00fchrt wird. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Docker falsch konfiguriert" + }, + "unsupported_docker_version": { + "description": "Das System wird nicht unterst\u00fctzt, da die falsche Version von Docker verwendet wird. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Docker-Version" + }, + "unsupported_job_conditions": { + "description": "Das System wird nicht unterst\u00fctzt, da eine oder mehrere Jobbedingungen deaktiviert wurden, die vor unerwarteten Ausf\u00e4llen und Unterbrechungen sch\u00fctzen. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Schutz deaktiviert" + }, + "unsupported_lxc": { + "description": "Das System wird nicht unterst\u00fctzt, da es in einer virtuellen LXC-Maschine ausgef\u00fchrt wird. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 LXC erkannt" + }, + "unsupported_network_manager": { + "description": "Das System wird nicht unterst\u00fctzt, weil Network Manager fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Probleme mit Network Manager" + }, + "unsupported_os": { + "description": "Das System wird nicht unterst\u00fctzt, da das verwendete Betriebssystem nicht f\u00fcr die Verwendung mit Supervisor getestet oder gewartet wurde. Verwende den Link, um zu erfahren, welche Betriebssysteme unterst\u00fctzt werden und wie du das Problem beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Betriebssystem" + }, + "unsupported_os_agent": { + "description": "Das System wird nicht unterst\u00fctzt, weil OS-Agent fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Probleme mit OS-Agenten" + }, + "unsupported_restart_policy": { + "description": "Das System wird nicht unterst\u00fctzt, da f\u00fcr einen Docker-Container eine Neustartrichtlinie festgelegt ist, die beim Start Probleme verursachen k\u00f6nnte. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Container-Neustartrichtlinie" + }, + "unsupported_software": { + "description": "Das System wird nicht unterst\u00fctzt, da zus\u00e4tzliche Software au\u00dferhalb des Home Assistant-\u00d6kosystems erkannt wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Nicht unterst\u00fctzte Software" + }, + "unsupported_source_mods": { + "description": "Das System wird nicht unterst\u00fctzt, da der Supervisor-Quellcode ge\u00e4ndert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Modifikation des Supervisor-Quellcodes" + }, + "unsupported_supervisor_version": { + "description": "Das System wird nicht unterst\u00fctzt, da eine veraltete Version von Supervisor verwendet wird und die automatische Aktualisierung deaktiviert wurde. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Supervisor-Version" + }, + "unsupported_systemd": { + "description": "System wird nicht unterst\u00fctzt, weil Systemd fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System - Systemd-Probleme" + }, + "unsupported_systemd_journal": { + "description": "Das System wird nicht unterst\u00fctzt, da das Systemd-Journal und/oder der Gateway-Dienst fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Systemd Journal-Probleme" + }, + "unsupported_systemd_resolved": { + "description": "Das System wird nicht unterst\u00fctzt, weil Systemd Resolved fehlt, inaktiv oder falsch konfiguriert ist. Verwende den Link, um mehr zu erfahren und wie du dies beheben kannst.", + "title": "Nicht unterst\u00fctztes System \u2013 Von Systemd behobene Probleme" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index 243467b9f228af..cdfe7f17a44085 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -13,7 +13,7 @@ "title": "Unhealthy system - Not privileged" }, "unhealthy_setup": { - "description": "System is currently because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.", + "description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.", "title": "Unhealthy system - Setup failed" }, "unhealthy_supervisor": { diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index f2aef9d7214b73..202a362fbbcdc8 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "Actualmente el sistema no est\u00e1 en buen estado debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que est\u00e1 mal y c\u00f3mo solucionarlo.", + "description": "Actualmente el sistema no est\u00e1 en buen estado debido a {reason}. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", "title": "Sistema en mal estado: {reason}" }, + "unhealthy_docker": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque Docker est\u00e1 configurado incorrectamente. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado - Docker mal configurado" + }, + "unhealthy_privileged": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque no tiene acceso privilegiado al runtime de Docker. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado - No privilegiado" + }, + "unhealthy_setup": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque la configuraci\u00f3n no se complet\u00f3. Hay varias razones por las que esto puede ocurrir, utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado: La configuraci\u00f3n fall\u00f3" + }, + "unhealthy_supervisor": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque fall\u00f3 un intento de actualizar Supervisor a la \u00faltima versi\u00f3n. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema en mal estado: La actualizaci\u00f3n del supervisor fall\u00f3" + }, + "unhealthy_untrusted": { + "description": "Actualmente el sistema no est\u00e1 en buen estado porque ha detectado c\u00f3digo o im\u00e1genes en uso que no son de confianza. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no saludable: C\u00f3digo no confiable" + }, "unsupported": { - "description": "El sistema no es compatible debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que esto significa y c\u00f3mo volver a un sistema compatible.", + "description": "El sistema no es compatible debido a {reason}. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", "title": "Sistema no compatible: {reason}" + }, + "unsupported_apparmor": { + "description": "El sistema no es compatible porque AppArmor no funciona correctamente y los complementos se ejecutan sin protecci\u00f3n ni seguridad. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con AppArmor" + }, + "unsupported_cgroup_version": { + "description": "El sistema no es compatible porque se est\u00e1 utilizando una versi\u00f3n incorrecta de Docker CGroup. Utiliza el enlace para conocer la versi\u00f3n correcta y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: versi\u00f3n CGroup" + }, + "unsupported_connectivity_check": { + "description": "El sistema no es compatible porque Home Assistant no puede determinar cu\u00e1ndo hay una conexi\u00f3n a Internet disponible. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Verificaci\u00f3n de conectividad deshabilitada" + }, + "unsupported_content_trust": { + "description": "El sistema no es compatible porque Home Assistant no puede verificar que el contenido que se est\u00e1 ejecutando sea confiable y que los atacantes no lo hayan modificado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Verificaci\u00f3n de confianza de contenido deshabilitada" + }, + "unsupported_dbus": { + "description": "El sistema no es compatible porque D-Bus no funciona correctamente. Muchas cosas fallan sin esto, ya que Supervisor no puede comunicarse con el host. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con D-Bus" + }, + "unsupported_dns_server": { + "description": "El sistema no es compatible porque el servidor DNS proporcionado no funciona correctamente y la opci\u00f3n de DNS de respaldo se ha deshabilitado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con el servidor DNS" + }, + "unsupported_docker_configuration": { + "description": "El sistema no es compatible porque el demonio de Docker se est\u00e1 ejecutando de forma inesperada. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Docker mal configurado" + }, + "unsupported_docker_version": { + "description": "El sistema no es compatible porque se est\u00e1 utilizando una versi\u00f3n incorrecta de Docker. Utiliza el enlace para conocer la versi\u00f3n correcta y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Versi\u00f3n de Docker" + }, + "unsupported_job_conditions": { + "description": "El sistema no es compatible porque se han deshabilitado una o m\u00e1s condiciones de trabajo que protegen contra fallos y roturas inesperadas. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Protecciones deshabilitadas" + }, + "unsupported_lxc": { + "description": "El sistema no es compatible porque se est\u00e1 ejecutando en una m\u00e1quina virtual LXC. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - LXC detectado" + }, + "unsupported_network_manager": { + "description": "El sistema no es compatible porque falta Network Manager, est\u00e1 inactivo o mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Network Manager" + }, + "unsupported_os": { + "description": "El sistema no es compatible porque el sistema operativo en uso no se ha probado ni mantenido para su uso con Supervisor. Utiliza el enlace para saber qu\u00e9 sistemas operativos son compatibles y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible - Sistema operativo" + }, + "unsupported_os_agent": { + "description": "El sistema no es compatible porque OS-Agent falta, est\u00e1 inactivo o mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con OS-Agent" + }, + "unsupported_restart_policy": { + "description": "El sistema no es compatible porque un contenedor Docker tiene un conjunto de pol\u00edticas de reinicio que podr\u00eda causar problemas en el inicio. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Pol\u00edtica de reinicio del contenedor" + }, + "unsupported_software": { + "description": "El sistema no es compatible porque se detect\u00f3 software adicional fuera del ecosistema de Home Assistant. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Software no compatible" + }, + "unsupported_source_mods": { + "description": "El sistema no es compatible porque se modific\u00f3 el c\u00f3digo fuente de Supervisor. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Modificaciones de la fuente del supervisor" + }, + "unsupported_supervisor_version": { + "description": "El sistema no es compatible porque se est\u00e1 utilizando una versi\u00f3n obsoleta de Supervisor y se ha desactivado la actualizaci\u00f3n autom\u00e1tica. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Versi\u00f3n de supervisor" + }, + "unsupported_systemd": { + "description": "El sistema no es compatible porque falta Systemd, est\u00e1 inactivo o est\u00e1 mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Systemd" + }, + "unsupported_systemd_journal": { + "description": "El sistema no es compatible porque Systemd Journal y/o el servicio de puerta de enlace faltan, est\u00e1n inactivos o mal configurados. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "El sistema no es compatible porque falta Systemd Resolved, est\u00e1 inactivo o est\u00e1 mal configurado. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n y c\u00f3mo solucionarlo.", + "title": "Sistema no compatible: Problemas con Systemd-Resolved" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index ea0f78c0c57c0d..d0acf51857c246 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "S\u00fcsteem ei ole praegu korras '{reason}' t\u00f5ttu. Kasuta linki, et saada rohkem teavet selle kohta, mis on valesti ja kuidas seda parandada.", + "description": "S\u00fcsteem ei ole praegu korras {reason} t\u00f5ttu. Kasuta linki, et saada rohkem teavet ja kuidas seda parandada.", "title": "Vigane s\u00fcsteem \u2013 {reason}" }, + "unhealthy_docker": { + "description": "S\u00fcsteem on praegu ebatervislik, sest Docker on valesti konfigureeritud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Ebatervislik s\u00fcsteem \u2013 Docker on valesti konfigureeritud" + }, + "unhealthy_privileged": { + "description": "S\u00fcsteem on praegu ebatervislik, sest tal puudub privilegeeritud juurdep\u00e4\u00e4s dokkerite k\u00e4ivituskoodile. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - \u00f5igused puuduvad" + }, + "unhealthy_setup": { + "description": "S\u00fcsteem on hetkel ebatervislik, sest seadistamist ei \u00f5nnestunud l\u00f5pule viia. Sellel v\u00f5ib olla mitu p\u00f5hjust, kasuta linki, et saada rohkem teavet ja teada, kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - seadistamine eba\u00f5nnestus" + }, + "unhealthy_supervisor": { + "description": "S\u00fcsteem on hetkel ebatervislik, sest Supervisori uuendamise katse viimasele versioonile eba\u00f5nnestus. Kasuta linki, et teada saada rohkem ja kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - Supervisori uuendamine eba\u00f5nnestus" + }, + "unhealthy_untrusted": { + "description": "S\u00fcsteem on hetkel ebatervislik, sest on tuvastanud kasutuses oleva ebausaldusv\u00e4\u00e4rse koodi v\u00f5i kujutiste kasutamise. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Ebaterve s\u00fcsteem - ebausaldusv\u00e4\u00e4rne kood" + }, "unsupported": { - "description": "S\u00fcsteemi ei toetata '{reason}' t\u00f5ttu. Kasuta linki, et saada lisateavet selle kohta, mida see t\u00e4hendab ja kuidas toetatud s\u00fcsteemi naasta.", + "description": "S\u00fcsteemi ei toetata {reason} t\u00f5ttu. Kasuta linki, et saada lisateavet ja kuidas seda parandada.", "title": "Toetamata s\u00fcsteem \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "S\u00fcsteem ei ole toetatud, sest AppArmor t\u00f6\u00f6tab valesti ja lisad t\u00f6\u00f6tavad kaitsmata ja ebaturvaliselt. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 AppArmori probleemid" + }, + "unsupported_cgroup_version": { + "description": "S\u00fcsteem ei ole toetatud, sest kasutusel on vale Docker CGroupi versioon. Kasuta linki, et teada saada, milline on \u00f5ige versioon ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 CGroupi versioon" + }, + "unsupported_connectivity_check": { + "description": "S\u00fcsteem ei ole toetatud, sest Home Assistant ei suuda kindlaks teha, millal interneti\u00fchendus on saadaval. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "S\u00fcsteemi ei toetata \u2013 \u00fchenduvuse kontroll on keelatud" + }, + "unsupported_content_trust": { + "description": "S\u00fcsteemi ei toetata, kuna Home Assistant ei saa kontrollida, kas k\u00e4itatav sisu on usaldusv\u00e4\u00e4rne ja seda pole r\u00fcndajad muutnud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 sisu usaldusv\u00e4\u00e4rsuse kontroll on keelatud" + }, + "unsupported_dbus": { + "description": "S\u00fcsteem ei ole toetatud, sest D-Bus t\u00f6\u00f6tab valesti. Paljud asjad ei \u00f5nnestu ilma selleta, sest Supervisor ei saa suhelda hostiga. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem - D-Bus probleemid" + }, + "unsupported_dns_server": { + "description": "S\u00fcsteem ei ole toetatud, sest pakutav DNS-server ei t\u00f6\u00f6ta \u00f5igesti ja varu-DNS-variant on v\u00e4lja l\u00fclitatud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 DNS-serveri probleemid" + }, + "unsupported_docker_configuration": { + "description": "S\u00fcsteem ei ole toetatud, sest Dockeri deemon t\u00f6\u00f6tab ootamatul viisil. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 Docker on valesti konfigureeritud" + }, + "unsupported_docker_version": { + "description": "S\u00fcsteemi ei toetata kuna kasutusel on vale Dockeri versioon. Kasuta linki, et saada teavet \u00f5ige versiooni ja selle parandamise kohta.", + "title": "Toetamata s\u00fcsteem \u2013 Dockeri versioon" + }, + "unsupported_job_conditions": { + "description": "S\u00fcsteemi ei toetata kuna \u00fcks v\u00f5i mitu t\u00f6\u00f6tingimust on blokeeritud, mis kaitsevad ootamatute rikete ja purunemiste eest. Kasuta linki, et saada lisateavet ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem - kaitsed v\u00e4lja l\u00fclitatud" + }, + "unsupported_lxc": { + "description": "S\u00fcsteemi ei toetata, kuna seda k\u00e4itatakse LXC virtuaalmasinas. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 tuvastati LXC" + }, + "unsupported_network_manager": { + "description": "S\u00fcsteemi ei toetata, kuna Network Manager puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 v\u00f5rguhalduri probleemid" + }, + "unsupported_os": { + "description": "S\u00fcsteem ei ole toetatud, sest kasutatavat operatsioonis\u00fcsteemi ei ole testitud ega hooldatud Supervisoriga kasutamiseks. Kasuta linki, milliseid operatsioonis\u00fcsteeme toetatakse ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 operatsioonis\u00fcsteem" + }, + "unsupported_os_agent": { + "description": "S\u00fcsteem ei ole toetatud, sest OS-Agent puudub, on mitteaktiivne v\u00f5i valesti konfigureeritud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 OS-agendi probleemid" + }, + "unsupported_restart_policy": { + "description": "S\u00fcsteem ei ole toetatud, sest Dockeri konteinerile on m\u00e4\u00e4ratud taask\u00e4ivitamise poliitika, mis v\u00f5ib k\u00e4ivitamisel probleeme tekitada. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 konteineri taask\u00e4ivitamise reegel" + }, + "unsupported_software": { + "description": "S\u00fcsteem ei ole toetatud, sest on tuvastatud lisatarkvara v\u00e4ljaspool Home Assistant'i \u00f6kos\u00fcsteemi. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 toetamata tarkvara" + }, + "unsupported_source_mods": { + "description": "S\u00fcsteem ei ole toetatud, sest Supervisori l\u00e4htekoodi on muudetud. Kasuta linki, et rohkem teada saada ja kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem \u2013 Supervisori allika muudatused" + }, + "unsupported_supervisor_version": { + "description": "S\u00fcsteem ei ole toetatud, sest kasutusel on vananenud Supervisori versioon ja automaatne uuendamine on v\u00e4lja l\u00fclitatud. Kasuta linki, et saada rohkem teavet ja teada, kuidas seda parandada.", + "title": "Toetamata s\u00fcsteem - Supervisori versioon" + }, + "unsupported_systemd": { + "description": "S\u00fcsteemi ei toetata kuna Systemd puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem - Systemd probleemid" + }, + "unsupported_systemd_journal": { + "description": "S\u00fcsteemi ei toetata kuna Systemd Journal ja/v\u00f5i l\u00fc\u00fcsiteenus puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 Systemd Journali probleemid" + }, + "unsupported_systemd_resolved": { + "description": "S\u00fcsteemi ei toetata kuna Systemd Resolved puudub, on passiivne v\u00f5i valesti konfigureeritud. Lisateabe saamiseks ja selle parandamiseks kasuta linki.", + "title": "Toetamata s\u00fcsteem \u2013 Systemd lahendatud probleemid" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 604a8ae59e63f1..14f2d995ff666e 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "A rendszer jelenleg renellenes \u00e1llapotban van '{reason}' miatt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet is megtudhat arr\u00f3l, hogy mi a probl\u00e9ma, \u00e9s hogyan jav\u00edthatja ki.", + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van a k\u00f6vetkez\u0151 miatt: {reason}. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", "title": "Rendellenes \u00e1llapot \u2013 {reason}" }, + "unhealthy_docker": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert a Docker helytelen\u00fcl van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Rendellenes rendszer \u2013 A Docker rosszul lett konfigur\u00e1lva" + }, + "unhealthy_privileged": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert nem rendelkezik emelt szint\u0171 hozz\u00e1f\u00e9r\u00e9ssel a Docker-futtat\u00f3k\u00f6rnyezethez. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Rendellenes rendszer - Nem privilegiz\u00e1lt" + }, + "unhealthy_setup": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert a telep\u00edt\u00e9s nem fejez\u0151d\u00f6tt be. Ennek sz\u00e1mos oka lehet, haszn\u00e1lja a linket, hogy t\u00f6bbet megtudjon, \u00e9s hogyan jav\u00edthatja ki ezt.", + "title": "Rendellenes rendszer \u2013 A telep\u00edt\u00e9s sikertelen" + }, + "unhealthy_supervisor": { + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van, mert a Supervisor leg\u00fajabb verzi\u00f3ra t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9s\u00e9nek k\u00eds\u00e9rlete sikertelen volt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Rendellenes rendszer \u2013 A Supervisor friss\u00edt\u00e9se nem siker\u00fclt" + }, + "unhealthy_untrusted": { + "description": "A rendszer jelenleg nem megfelel\u0151, mert nem megb\u00edzhat\u00f3 k\u00f3dot vagy haszn\u00e1latban l\u00e9v\u0151 image-et \u00e9szlelt. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Rendellenes rendszer - Nem megb\u00edzhat\u00f3 k\u00f3d" + }, "unsupported": { - "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: '{reason}'. A hivatkoz\u00e1s seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat arr\u00f3l, mit jelent ez, \u00e9s hogyan t\u00e9rhet vissza egy t\u00e1mogatott rendszerhez.", + "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: {reason} . Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "A rendszer nem t\u00e1mogatott, mert az AppArmor helytelen\u00fcl m\u0171k\u00f6dik, \u00e9s a b\u0151v\u00edtm\u00e9nyek nem v\u00e9dett \u00e9s nem biztons\u00e1gos m\u00f3don futnak. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Nem t\u00e1mogatott rendszer \u2013 AppArmor-probl\u00e9m\u00e1k" + }, + "unsupported_cgroup_version": { + "description": "A rendszer nem t\u00e1mogatott, mert a Docker CGroup nem megfelel\u0151 verzi\u00f3ja van haszn\u00e1latban. A link seg\u00edts\u00e9g\u00e9vel megtudhatja a helyes verzi\u00f3t \u00e9s a jav\u00edt\u00e1s m\u00f3dj\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - CGroup verzi\u00f3" + }, + "unsupported_connectivity_check": { + "description": "A rendszer nem t\u00e1mogatott, mert az Otthoni asszisztens nem tudja meghat\u00e1rozni, hogy van-e m\u0171k\u00f6d\u0151 internetkapcsolat. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Nem t\u00e1mogatott rendszer - Csatlakoz\u00e1si ellen\u0151rz\u00e9s letiltva" + }, + "unsupported_content_trust": { + "description": "A rendszer nem t\u00e1mogatott, mivel a Home Assistant nem tudja ellen\u0151rizni, hogy a futtatott tartalom megb\u00edzhat\u00f3 \u00e9s nem t\u00e1mad\u00f3k \u00e1ltal m\u00f3dos\u00edtott. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Tartalom-megb\u00edzhat\u00f3s\u00e1gi ellen\u0151rz\u00e9s letiltva" + }, + "unsupported_dbus": { + "description": "A rendszer nem t\u00e1mogatott, mert a D-Bus hib\u00e1san m\u0171k\u00f6dik. E n\u00e9lk\u00fcl sok minden meghib\u00e1sodik, mivel a Supervisor nem tud kommunik\u00e1lni a rendszerrel. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan lehet ezt kijav\u00edtani.", + "title": "Nem t\u00e1mogatott rendszer - D-Bus probl\u00e9m\u00e1k" + }, + "unsupported_dns_server": { + "description": "A rendszer nem t\u00e1mogatott, mert a megadott DNS-kiszolg\u00e1l\u00f3 nem m\u0171k\u00f6dik megfelel\u0151en, \u00e9s a tartal\u00e9k DNS opci\u00f3t letiltott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - DNS-kiszolg\u00e1l\u00f3 probl\u00e9m\u00e1k" + }, + "unsupported_docker_configuration": { + "description": "A rendszer nem t\u00e1mogatott, mert a Docker d\u00e9mon nem az elv\u00e1rt m\u00f3don fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer \u2013 A Docker helytelen\u00fcl van konfigur\u00e1lva" + }, + "unsupported_docker_version": { + "description": "A rendszer nem t\u00e1mogatott, mert a Docker nem megfelel\u0151 verzi\u00f3ja van haszn\u00e1latban. A link seg\u00edts\u00e9g\u00e9vel megtudhatja a helyes verzi\u00f3t \u00e9s a jav\u00edt\u00e1s m\u00f3dj\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Docker verzi\u00f3" + }, + "unsupported_job_conditions": { + "description": "A rendszer nem t\u00e1mogatott, mert egy vagy t\u00f6bb feladatfelt\u00e9tel le van tiltva, amelyek v\u00e9delmet ny\u00fajtanak a v\u00e1ratlan hib\u00e1kt\u00f3l. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", + "title": "Nem t\u00e1mogatott rendszer \u2013 A v\u00e9delem le van tiltva" + }, + "unsupported_lxc": { + "description": "A rendszer nem t\u00e1mogatott, mert LXC virtu\u00e1lis g\u00e9pben fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - LXC \u00e9szlelve" + }, + "unsupported_network_manager": { + "description": "A rendszer nem t\u00e1mogatott, mert a H\u00e1l\u00f3zatkezel\u0151 hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Network Manager probl\u00e9m\u00e1k" + }, + "unsupported_os": { + "description": "A rendszer nem t\u00e1mogatott, mert a haszn\u00e1lt oper\u00e1ci\u00f3s rendszert nem tesztelt\u00e9k vagy nem tartj\u00e1k karban a Supervisorral val\u00f3 haszn\u00e1latra. Haszn\u00e1lja a linket, hogy mely oper\u00e1ci\u00f3s rendszerek t\u00e1mogatottak \u00e9s hogyan lehet ezt kijav\u00edtani.", + "title": "Nem t\u00e1mogatott rendszer - Oper\u00e1ci\u00f3s rendszer" + }, + "unsupported_os_agent": { + "description": "A rendszer nem t\u00e1mogatott, mert az OS-\u00dcgyn\u00f6k (agent) hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - OS-\u00dcgyn\u00f6k probl\u00e9m\u00e1k" + }, + "unsupported_restart_policy": { + "description": "A rendszer nem t\u00e1mogatott, mivel a Docker kont\u00e9nerben olyan \u00fajraind\u00edt\u00e1si h\u00e1zirend van be\u00e1ll\u00edtva, amely ind\u00edt\u00e1skor probl\u00e9m\u00e1kat okozhat. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Kont\u00e9ner \u00fajraind\u00edt\u00e1si szab\u00e1lyzat" + }, + "unsupported_software": { + "description": "A rendszer nem t\u00e1mogatott, mert a rendszer a Home Assistant \u00f6kosziszt\u00e9m\u00e1n k\u00edv\u00fcli tov\u00e1bbi szoftvereket \u00e9szlelt. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja ezt.", + "title": "Rendellenes rendszer - Nem t\u00e1mogatott szoftver" + }, + "unsupported_source_mods": { + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor forr\u00e1sk\u00f3dj\u00e1t m\u00f3dos\u00edtott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Supervisor forr\u00e1sm\u00f3dos\u00edt\u00e1sok" + }, + "unsupported_supervisor_version": { + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor egy elavult verzi\u00f3ja van haszn\u00e1latban, \u00e9s az automatikus friss\u00edt\u00e9s le van tiltva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Supervisor verzi\u00f3" + }, + "unsupported_systemd": { + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Systemd probl\u00e9m\u00e1k" + }, + "unsupported_systemd_journal": { + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Journal \u00e9s/vagy az \u00e1tj\u00e1r\u00f3 szolg\u00e1ltat\u00e1s hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva . A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer - Systemd Journal probl\u00e9m\u00e1k" + }, + "unsupported_systemd_resolved": { + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Resolved hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "title": "Nem t\u00e1mogatott rendszer \u2013 Systemd Resolved probl\u00e9m\u00e1k" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/id.json b/homeassistant/components/hassio/translations/id.json index 250e6e7d4ad6ed..f18ae6f84c6bc2 100644 --- a/homeassistant/components/hassio/translations/id.json +++ b/homeassistant/components/hassio/translations/id.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "Sistem saat ini tidak sehat karena {reason}. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - {reason}" + }, + "unhealthy_docker": { + "description": "Sistem saat ini tidak sehat karena Docker tidak dikonfigurasi dengan benar. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Docker salah dikonfigurasi" + }, + "unhealthy_privileged": { + "description": "Sistem saat ini tidak sehat karena tidak memiliki akses istimewa ke docker runtime. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Tidak memiliki akses istimewa" + }, + "unhealthy_setup": { + "description": "Sistem saat ini tidak sehat karena penyiapan gagal diselesaikan. Ada sejumlah alasan mengapa hal ini bisa terjadi, gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Penyiapan gagal" + }, + "unhealthy_supervisor": { + "description": "Sistem saat ini tidak sehat karena upaya untuk memperbarui Supervisor ke versi terbaru telah gagal. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Pembaruan supervisor gagal" + }, + "unhealthy_untrusted": { + "description": "Sistem saat ini tidak sehat karena telah mendeteksi kode atau gambar yang tidak dipercaya yang digunakan. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak sehat - Kode tidak tepercaya" + }, + "unsupported": { + "description": "Sistem tidak didukung karena {reason}. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - {reason}" + }, + "unsupported_apparmor": { + "description": "Sistem tidak didukung karena AppArmor tidak berfungsi dengan benar dan add-on berjalan dengan cara yang tidak terlindungi dan tidak aman. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah AppArmor" + }, + "unsupported_cgroup_version": { + "description": "Sistem tidak didukung karena versi Docker CGroup yang digunakan salah. Gunakan tautan untuk mempelajari versi yang benar dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Versi CGroup" + }, + "unsupported_connectivity_check": { + "description": "Sistem tidak didukung karena Home Assistant tidak dapat menentukan kapan koneksi internet tersedia. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Pemeriksaan konektivitas dinonaktifkan" + }, + "unsupported_content_trust": { + "description": "Sistem tidak didukung karena Home Assistant tidak dapat memverifikasi konten yang sedang dijalankan adalah dipercaya dan tidak dimodifikasi oleh penyerang. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Pemeriksaan kepercayaan konten dinonaktifkan" + }, + "unsupported_dbus": { + "description": "Sistem tidak didukung karena D-Bus bekerja secara tidak benar. Banyak hal gagal tanpa D-Bus ini karena Supervisor tidak dapat berkomunikasi dengan host. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah D-Bus" + }, + "unsupported_dns_server": { + "description": "Sistem tidak didukung karena server DNS yang disediakan tidak berfungsi dengan benar dan opsi DNS fallback telah dinonaktifkan. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah server DNS" + }, + "unsupported_docker_configuration": { + "description": "Sistem tidak didukung karena daemon Docker berjalan dengan cara yang tidak terduga. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Docker salah konfigurasi" + }, + "unsupported_docker_version": { + "description": "Sistem tidak didukung karena versi Docker yang salah sedang digunakan. Gunakan tautan untuk mempelajari versi yang benar dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Versi Docker" + }, + "unsupported_job_conditions": { + "description": "Sistem tidak didukung karena satu atau beberapa kondisi pekerjaan telah dinonaktifkan, yang melindungi dari kegagalan dan kerusakan yang tidak terduga. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Perlindungan dinonaktifkan" + }, + "unsupported_lxc": { + "description": "Sistem tidak didukung karena dijalankan di mesin virtual LXC. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - LXC terdeteksi" + }, + "unsupported_network_manager": { + "description": "Sistem tidak didukung karena Network Manager tidak ada, tidak aktif atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Manajer Jaringan" + }, + "unsupported_os": { + "description": "Sistem tidak didukung karena sistem operasi yang digunakan tidak diuji atau dipelihara untuk digunakan dengan Supervisor. Gunakan tautan ke sistem operasi mana yang didukung dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Sistem Operasi" + }, + "unsupported_os_agent": { + "description": "Sistem tidak didukung karena OS-Agent tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah OS-Agent" + }, + "unsupported_restart_policy": { + "description": "Sistem tidak didukung karena kontainer Docker memiliki kebijakan mulai ulang yang ditetapkan, yang dapat menyebabkan masalah saat mulai. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Kebijakan mulai ulang kontainer" + }, + "unsupported_software": { + "description": "Sistem tidak didukung karena perangkat lunak tambahan di luar ekosistem Home Assistant telah terdeteksi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Perangkat lunak tidak didukung" + }, + "unsupported_source_mods": { + "description": "Sistem tidak didukung karena kode sumber Supervisor telah dimodifikasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Modifikasi kode sumber Supervisor" + }, + "unsupported_supervisor_version": { + "description": "Sistem tidak didukung karena versi Supervisor yang kedaluwarsa sedang digunakan dan pembaruan otomatis telah dinonaktifkan. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Versi Supervisor" + }, + "unsupported_systemd": { + "description": "Sistem tidak didukung karena Systemd tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Systemd" + }, + "unsupported_systemd_journal": { + "description": "Sistem tidak didukung karena Jurnal Systemd dan/atau layanan gateway tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Journal Systemd" + }, + "unsupported_systemd_resolved": { + "description": "Sistem tidak didukung karena Systemd Resolved tidak ada, tidak aktif, atau salah dikonfigurasi. Gunakan tautan untuk mempelajari lebih lanjut dan cara memperbaikinya.", + "title": "Sistem tidak didukung - Masalah Systemd-Resolved" + } + }, "system_health": { "info": { "agent_version": "Versi Agen", diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index c305b9fce34759..20460b1d4dc628 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "Il sistema non \u00e8 attualmente integro a causa di '{reason}'. Usa il collegamento per saperne di pi\u00f9 sul problema e su come risolverlo.", + "description": "Il sistema non \u00e8 attualmente integro a causa di {reason}. Usa il collegamento per saperne di pi\u00f9 e come risolvere il problema.", "title": "Sistema non pi\u00f9 integro - {reason}" }, + "unhealthy_docker": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 Docker \u00e8 configurato in modo errato. Usa il link per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Docker configurato in modo errato" + }, + "unhealthy_privileged": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 non ha accesso privilegiato al runtime docker. Usa il link per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Non privilegiato" + }, + "unhealthy_setup": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 l'installazione non \u00e8 stata completata. Ci sono una serie di ragioni per cui ci\u00f2 pu\u00f2 verificarsi, usa il link per saperne di pi\u00f9 e come risolverlo.", + "title": "Sistema non integro - Installazione non riuscita" + }, + "unhealthy_supervisor": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 un tentativo di aggiornare Supervisor all'ultima versione non \u00e8 riuscito. Utilizza il collegamento per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Aggiornamento del Supervisor non riuscito" + }, + "unhealthy_untrusted": { + "description": "Il sistema non \u00e8 attualmente integro perch\u00e9 ha rilevato codice o immagini in uso non attendibili. Utilizza il collegamento per saperne di pi\u00f9 e come risolvere questo problema.", + "title": "Sistema non integro - Codice non attendibile" + }, "unsupported": { - "description": "Il sistema non \u00e8 supportato a causa di '{reason}'. Utilizzare il collegamento per ulteriori informazioni sul significato e su come tornare a un sistema supportato.", + "description": "Il sistema non \u00e8 supportato a causa di {reason}. Utilizzare il collegamento per ulteriori informazioni sul significato e su come tornare a un sistema supportato.", "title": "Sistema non supportato - {reason}" + }, + "unsupported_apparmor": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 AppArmor non funziona correttamente e i componenti aggiuntivi sono eseguiti in modo non protetto e non sicuro. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con AppArmor" + }, + "unsupported_cgroup_version": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 in uso una versione errata di Docker CGroup. Utilizza il collegamento per conoscere la versione corretta e come risolvere il problema.", + "title": "Sistema non supportato - Versione di CGroup" + }, + "unsupported_connectivity_check": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Home Assistant non \u00e8 in grado di determinare quando \u00e8 disponibile una connessione a Internet. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Controllo connettivit\u00e0 disabilitato" + }, + "unsupported_content_trust": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Home Assistant non \u00e8 in grado di verificare che il contenuto in esecuzione sia attendibile e non modificato da malintenzionati. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Controllo dell'attendibilit\u00e0 del contenuto disabilitato" + }, + "unsupported_dbus": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 D-Bus non funziona correttamente. Molte cose non funzionano senza di esso perch\u00e9 il Supervisor non pu\u00f2 comunicare con l'host. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con D-Bus" + }, + "unsupported_dns_server": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il server DNS fornito non funziona correttamente e l'opzione DNS di fallback \u00e8 stata disattivata. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con il server DNS" + }, + "unsupported_docker_configuration": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il daemon Docker viene eseguito in modo imprevisto. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Docker configurato in modo errato" + }, + "unsupported_docker_version": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 in uso una versione errata di Docker. Utilizza il collegamento per conoscere la versione corretta e come risolvere il problema.", + "title": "Sistema non supportato - Versione Docker" + }, + "unsupported_job_conditions": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 sono state disattivate una o pi\u00f9 condizioni di lavoro che proteggono da guasti e rotture impreviste. Utilizza il collegamento per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Protezioni disabilitate" + }, + "unsupported_lxc": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 viene eseguito in una macchina virtuale LXC. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - rilevato LXC" + }, + "unsupported_network_manager": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Network Manager \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Network Manager" + }, + "unsupported_os": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il sistema operativo in uso non \u00e8 stato testato o mantenuto per l'uso con Supervisor. Utilizza il link per sapere quali sistemi operativi sono supportati e come risolvere il problema.", + "title": "Sistema non supportato - Sistema operativo" + }, + "unsupported_os_agent": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 OS-Agent \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con OS-Agent" + }, + "unsupported_restart_policy": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 un container Docker ha impostato un criterio di riavvio che potrebbe causare problemi all'avvio. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Criterio di riavvio del Container" + }, + "unsupported_software": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 stato rilevato un software aggiuntivo esterno all'ecosistema Home Assistant. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Software non supportato" + }, + "unsupported_source_mods": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 il codice sorgente del Supervisor \u00e8 stato modificato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Modifiche al codice sorgente del Supervisor" + }, + "unsupported_supervisor_version": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 \u00e8 in uso una versione non aggiornata del Supervisor e l'aggiornamento automatico \u00e8 stato disabilitato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Versione Supervisor" + }, + "unsupported_systemd": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Systemd \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Systemd" + }, + "unsupported_systemd_journal": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Systemd Journal e/o il servizio gateway sono mancanti, inattivi o mal configurati. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "Il sistema non \u00e8 supportato perch\u00e9 Systemd Resolved \u00e8 mancante, inattivo o mal configurato. Utilizza il link per saperne di pi\u00f9 e per capire come risolvere il problema.", + "title": "Sistema non supportato - Problemi con Systemd Resolved" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index 8dd5d471860402..ee5d5328085d7f 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "Systemet er for \u00f8yeblikket usunt p\u00e5 grunn av '{reason}'. Bruk linken for \u00e5 l\u00e6re mer om hva som er galt og hvordan du kan fikse det.", + "description": "Systemet er for \u00f8yeblikket usunt p\u00e5 grunn av {reason} . Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", "title": "Usunt system \u2013 {reason}" }, + "unhealthy_docker": { + "description": "Systemet er for \u00f8yeblikket usunt fordi Docker er feil konfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Docker feilkonfigurert" + }, + "unhealthy_privileged": { + "description": "Systemet er for \u00f8yeblikket usunt fordi det ikke har privilegert tilgang til docker-kj\u00f8retiden. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Ikke privilegert" + }, + "unhealthy_setup": { + "description": "Systemet er for \u00f8yeblikket usunt fordi oppsettet ikke ble fullf\u00f8rt. Det er flere grunner til at dette kan skje. Bruk lenken for \u00e5 l\u00e6re mer og hvordan du kan fikse dette.", + "title": "Usunt system - Konfigurasjonen mislyktes" + }, + "unhealthy_supervisor": { + "description": "Systemet er for \u00f8yeblikket usunt fordi et fors\u00f8k p\u00e5 \u00e5 oppdatere Supervisor til den nyeste versjonen har mislyktes. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Supervisor-oppdatering mislyktes" + }, + "unhealthy_untrusted": { + "description": "Systemet er for \u00f8yeblikket usunt fordi det har oppdaget uklarert kode eller bilder som er i bruk. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Usunt system - Ubetrodd kode" + }, "unsupported": { - "description": "Systemet st\u00f8ttes ikke p\u00e5 grunn av '{reason}'. Bruk lenken for \u00e5 l\u00e6re mer om hva dette betyr og hvordan du g\u00e5r tilbake til et st\u00f8ttet system.", + "description": "Systemet st\u00f8ttes ikke p\u00e5 grunn av {reason} . Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", "title": "Systemet st\u00f8ttes ikke \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "Systemet st\u00f8ttes ikke fordi AppArmor fungerer feil og tillegg kj\u00f8rer p\u00e5 en ubeskyttet og usikker m\u00e5te. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 AppArmor-problemer" + }, + "unsupported_cgroup_version": { + "description": "Systemet st\u00f8ttes ikke fordi feil versjon av Docker CGroup er i bruk. Bruk linken for \u00e5 l\u00e6re riktig versjon og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes - CGroup-versjon" + }, + "unsupported_connectivity_check": { + "description": "Systemet st\u00f8ttes ikke fordi Home Assistant ikke kan fastsl\u00e5 n\u00e5r en Internett-tilkobling er tilgjengelig. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 tilkoblingskontroll er deaktivert" + }, + "unsupported_content_trust": { + "description": "Systemet st\u00f8ttes ikke fordi Home Assistant ikke kan bekrefte at innhold som kj\u00f8res er klarert og ikke endret av angripere. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 kontroll av innhold og klarering er deaktivert" + }, + "unsupported_dbus": { + "description": "Systemet st\u00f8ttes ikke fordi D-Bus fungerer feil. Mange ting feiler uten dette da veileder ikke kan kommunisere med verten. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Ust\u00f8ttet system - D-Bus-problemer" + }, + "unsupported_dns_server": { + "description": "Systemet st\u00f8ttes ikke fordi den angitte DNS-serveren ikke fungerer som den skal og alternativet for reserve-DNS er deaktivert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 DNS-serverproblemer" + }, + "unsupported_docker_configuration": { + "description": "Systemet st\u00f8ttes ikke fordi Docker-demonen kj\u00f8rer p\u00e5 en uventet m\u00e5te. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Docker er feilkonfigurert" + }, + "unsupported_docker_version": { + "description": "Systemet st\u00f8ttes ikke fordi feil versjon av Docker er i bruk. Bruk linken for \u00e5 l\u00e6re riktig versjon og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Docker-versjon" + }, + "unsupported_job_conditions": { + "description": "Systemet st\u00f8ttes ikke fordi en eller flere jobbbetingelser er deaktivert som beskytter mot uventede feil og brudd. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Systemet som ikke st\u00f8ttes \u2013 Beskyttelse er deaktivert" + }, + "unsupported_lxc": { + "description": "Systemet st\u00f8ttes ikke fordi det kj\u00f8res i en virtuell LXC-maskin. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 LXC oppdaget" + }, + "unsupported_network_manager": { + "description": "Systemet st\u00f8ttes ikke fordi Network Manager mangler, er inaktiv eller feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 problemer med Network Manager" + }, + "unsupported_os": { + "description": "Systemet st\u00f8ttes ikke fordi operativsystemet som er i bruk ikke er testet eller vedlikeholdt for bruk med Supervisor. Bruk lenken til hvilke operativsystemer som st\u00f8ttes og hvordan du fikser dette.", + "title": "Ikke-st\u00f8ttet system - Operativsystem" + }, + "unsupported_os_agent": { + "description": "Systemet st\u00f8ttes ikke fordi OS-Agent mangler, er inaktivt eller feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 OS-Agent-problemer" + }, + "unsupported_restart_policy": { + "description": "Systemet st\u00f8ttes ikke fordi en Docker-beholder har et omstartspolicysett som kan for\u00e5rsake problemer ved oppstart. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 policy for omstart av container" + }, + "unsupported_software": { + "description": "Systemet st\u00f8ttes ikke fordi tilleggsprogramvare utenfor Home Assistant-\u00f8kosystemet er oppdaget. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Systemet som ikke st\u00f8ttes \u2013 programvare som ikke st\u00f8ttes" + }, + "unsupported_source_mods": { + "description": "Systemet st\u00f8ttes ikke fordi Supervisor-kildekoden er endret. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 endringer i veilederkilder" + }, + "unsupported_supervisor_version": { + "description": "Systemet st\u00f8ttes ikke fordi en utdatert versjon av Supervisor er i bruk og automatisk oppdatering er deaktivert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes - Supervisor-versjon" + }, + "unsupported_systemd": { + "description": "Systemet st\u00f8ttes ikke fordi Systemd mangler, er inaktivt eller er feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Systemd-problemer" + }, + "unsupported_systemd_journal": { + "description": "Systemet st\u00f8ttes ikke fordi Systemd Journal og/eller gatewaytjenesten mangler, er inaktiv eller feilkonfigurert . Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "Systemet som ikke st\u00f8ttes \u2013 Systemd Journal-problemer" + }, + "unsupported_systemd_resolved": { + "description": "Systemet st\u00f8ttes ikke fordi Systemd Resolved mangler, er inaktivt eller feilkonfigurert. Bruk linken for \u00e5 l\u00e6re mer og hvordan du fikser dette.", + "title": "System som ikke st\u00f8ttes \u2013 Systemd-l\u00f8ste problemer" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json index 47e0b6df4aed6b..9696b9ca2bbd7b 100644 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a '{reason}'. Use o link para saber mais sobre o que est\u00e1 errado e como corrigi-lo.", + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a {reason}. Use o link para saber mais e como corrigir isso.", "title": "Sistema insalubre - {reason}" }, + "unhealthy_docker": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque o Docker est\u00e1 configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o \u00edntegro - Docker mal configurado" + }, + "unhealthy_privileged": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque n\u00e3o tem acesso privilegiado ao tempo de execu\u00e7\u00e3o do docker. Use o link para saber mais e como corrigir isso.", + "title": "Sistema insalubre - N\u00e3o privilegiado" + }, + "unhealthy_setup": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque a instala\u00e7\u00e3o n\u00e3o foi conclu\u00edda. Existem v\u00e1rias raz\u00f5es pelas quais isso pode ocorrer, use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o \u00edntegro - Falha na configura\u00e7\u00e3o" + }, + "unhealthy_supervisor": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque uma tentativa de atualizar o Supervisor para a vers\u00e3o mais recente falhou. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o \u00edntegro - Falha na atualiza\u00e7\u00e3o do supervisor" + }, + "unhealthy_untrusted": { + "description": "O sistema n\u00e3o est\u00e1 \u00edntegro no momento porque detectou c\u00f3digo ou imagens n\u00e3o confi\u00e1veis em uso. Use o link para saber mais e como corrigir isso.", + "title": "Sistema insalubre - C\u00f3digo n\u00e3o confi\u00e1vel" + }, "unsupported": { - "description": "O sistema n\u00e3o \u00e9 suportado devido a '{reason}'. Use o link para saber mais sobre o que isso significa e como retornar a um sistema compat\u00edvel.", + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel devido a {reason}. Use o link para saber mais e como corrigir isso.", "title": "Sistema n\u00e3o suportado - {reason}" + }, + "unsupported_apparmor": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o AppArmor est\u00e1 funcionando incorretamente e os complementos est\u00e3o sendo executados de maneira desprotegida e insegura. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas no AppArmor" + }, + "unsupported_cgroup_version": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque a vers\u00e3o errada do Docker CGroup est\u00e1 em uso. Use o link para saber a vers\u00e3o correta e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Vers\u00e3o do CGroup" + }, + "unsupported_connectivity_check": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o Home Assistant n\u00e3o pode determinar quando uma conex\u00e3o com a Internet est\u00e1 dispon\u00edvel. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Verifica\u00e7\u00e3o de conectividade desativada" + }, + "unsupported_content_trust": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o Home Assistant n\u00e3o pode verificar se o conte\u00fado em execu\u00e7\u00e3o \u00e9 confi\u00e1vel e n\u00e3o foi modificado por invasores. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Verifica\u00e7\u00e3o de confian\u00e7a de conte\u00fado desabilitada" + }, + "unsupported_dbus": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o D-Bus est\u00e1 funcionando incorretamente. Muitas coisas falham sem isso, pois o Supervisor n\u00e3o pode se comunicar com o host. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas de D-Bus" + }, + "unsupported_dns_server": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o servidor DNS fornecido n\u00e3o funciona corretamente e a op\u00e7\u00e3o DNS de fallback foi desativada. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas no servidor DNS" + }, + "unsupported_docker_configuration": { + "description": "O sistema n\u00e3o tem suporte porque o daemon do Docker est\u00e1 sendo executado de maneira inesperada. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Docker configurado incorretamente" + }, + "unsupported_docker_version": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque a vers\u00e3o errada do Docker est\u00e1 em uso. Use o link para saber a vers\u00e3o correta e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Vers\u00e3o do Docker" + }, + "unsupported_job_conditions": { + "description": "O sistema n\u00e3o \u00e9 suportado porque uma ou mais condi\u00e7\u00f5es de trabalho foram desativadas, protegendo contra falhas e quebras inesperadas. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Prote\u00e7\u00f5es desativadas" + }, + "unsupported_lxc": { + "description": "O sistema n\u00e3o \u00e9 suportado porque est\u00e1 sendo executado em uma m\u00e1quina virtual LXC. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - LXC detectado" + }, + "unsupported_network_manager": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Network Manager est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas no Network Manager" + }, + "unsupported_os": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque o sistema operacional em uso n\u00e3o foi testado ou mantido para uso com o Supervisor. Use o link para quais sistemas operacionais s\u00e3o suportados e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Sistema operacional" + }, + "unsupported_os_agent": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o OS Agent est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Problemas com o OS Agent" + }, + "unsupported_restart_policy": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque um cont\u00eainer do Docker tem uma pol\u00edtica de reinicializa\u00e7\u00e3o definida que pode causar problemas na inicializa\u00e7\u00e3o. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o compat\u00edvel - Pol\u00edtica de reinicializa\u00e7\u00e3o de cont\u00eainer" + }, + "unsupported_software": { + "description": "O sistema n\u00e3o \u00e9 compat\u00edvel porque foi detectado software adicional fora do ecossistema do Home Assistant. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Software n\u00e3o suportado" + }, + "unsupported_source_mods": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o c\u00f3digo-fonte do Supervisor foi modificado. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Modifica\u00e7\u00f5es de origem do supervisor" + }, + "unsupported_supervisor_version": { + "description": "O sistema n\u00e3o \u00e9 suportado porque uma vers\u00e3o desatualizada do Supervisor est\u00e1 em uso e a atualiza\u00e7\u00e3o autom\u00e1tica foi desabilitada. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - Vers\u00e3o do Supervisor" + }, + "unsupported_systemd": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Systemd est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - problemas no Systemd" + }, + "unsupported_systemd_journal": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Systemd Journal e/ou o servi\u00e7o de gateway est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - problemas do Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "O sistema n\u00e3o \u00e9 suportado porque o Systemd Resolved est\u00e1 ausente, inativo ou configurado incorretamente. Use o link para saber mais e como corrigir isso.", + "title": "Sistema n\u00e3o suportado - problemas resolvidos pelo Systemd" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index 0ab366c1775ef0..e41f77b51ef61a 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", - "title": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 {reason}. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + }, + "unhealthy_docker": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a Docker \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Docker" + }, + "unhealthy_privileged": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0441\u0440\u0435\u0434\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Docker. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u0442 \u043f\u0440\u0438\u0432\u0438\u043b\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "unhealthy_setup": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\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. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u043e\u0439\u0442\u0438 \u043f\u043e \u0440\u044f\u0434\u0443 \u043f\u0440\u0438\u0447\u0438\u043d, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0441\u0431\u043e\u0439 \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435" + }, + "unhealthy_supervisor": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c Supervisor \u0434\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u0432\u0435\u0440\u0441\u0438\u0438. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0441\u0431\u043e\u0439 \u043f\u0440\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 Supervisor" + }, + "unhealthy_untrusted": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u043b\u0438 \u043e\u0431\u0440\u0430\u0437\u044b. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434" }, "unsupported": { - "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0438 \u043a\u0430\u043a \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", - "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}" + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 {reason}. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 {reason}" + }, + "unsupported_apparmor": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 AppArmor \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e, \u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f \u043d\u0435\u0437\u0430\u0449\u0438\u0449\u0435\u043d\u043d\u044b\u043c \u0438 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 AppArmor" + }, + "unsupported_cgroup_version": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Docker CGroup. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0432\u0435\u0440\u0441\u0438\u044f CGroup" + }, + "unsupported_connectivity_check": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Home Assistant \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u043b\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "unsupported_content_trust": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Home Assistant \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043b\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u044b\u043c \u0438 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0451\u043d \u0437\u043b\u043e\u0443\u043c\u044b\u0448\u043b\u0435\u043d\u043d\u0438\u043a\u0430\u043c\u0438. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430" + }, + "unsupported_dbus": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 D-Bus \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e. \u041e\u0442 \u044d\u0442\u043e\u0433\u043e Supervisor \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f \u0441 \u0445\u043e\u0441\u0442\u043e\u043c \u0438 \u043c\u043d\u043e\u0433\u043e\u0435 \u043c\u043e\u0436\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 D-Bus" + }, + "unsupported_dns_server": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 DNS-\u0441\u0435\u0440\u0432\u0435\u0440 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e, \u0430 DNS fallback \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 DNS-\u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c" + }, + "unsupported_docker_configuration": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0441\u043b\u0443\u0436\u0431\u0430 Docker \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0435\u043f\u0440\u0435\u0434\u0443\u0441\u043c\u043e\u0442\u0440\u0435\u043d\u043d\u044b\u043c \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Docker" + }, + "unsupported_docker_version": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Docker. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0432\u0435\u0440\u0441\u0438\u044f Docker" + }, + "unsupported_job_conditions": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043e\u0434\u043d\u043e \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u043b\u043e\u0432\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0437\u0430\u0449\u0438\u0449\u0430\u044e\u0442 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u043e\u0442 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u044b\u0445 \u0441\u0431\u043e\u0435\u0432. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0437\u0430\u0449\u0438\u0442\u0430" + }, + "unsupported_lxc": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u0437\u0430\u043f\u0443\u0449\u0435\u043d\u0430 \u043d\u0430 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0439 \u043c\u0430\u0448\u0438\u043d\u0435 LXC. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 LXC" + }, + "unsupported_network_manager": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Network Manager \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 Network Manager" + }, + "unsupported_os": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043b\u0430\u0441\u044c \u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0441 Supervisor. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430" + }, + "unsupported_os_agent": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 OS-Agent \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 OS-Agent" + }, + "unsupported_restart_policy": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0435 Docker \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430" + }, + "unsupported_software": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0435 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u0435, \u043d\u0435 \u0432\u0445\u043e\u0434\u044f\u0449\u0435\u0435 \u0432 \u044d\u043a\u043e\u0441\u0438\u0441\u0442\u0435\u043c\u0443 Home Assistant. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u041f\u041e" + }, + "unsupported_source_mods": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 Supervisor \u0431\u044b\u043b \u0438\u0437\u043c\u0435\u043d\u0451\u043d. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043c\u043e\u0434\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 Supervisor" + }, + "unsupported_supervisor_version": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f Supervisor, \u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u0432\u0435\u0440\u0441\u0438\u044f Supervisor" + }, + "unsupported_systemd": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Systemd \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 Systemd" + }, + "unsupported_systemd_journal": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0436\u0443\u0440\u043d\u0430\u043b Systemd \u0438/\u0438\u043b\u0438 \u0441\u043b\u0443\u0436\u0431\u0430 \u0448\u043b\u044e\u0437\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0436\u0443\u0440\u043d\u0430\u043b\u043e\u043c Systemd" + }, + "unsupported_systemd_resolved": { + "description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 Systemd Resolved \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442, \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u2014 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 Systemd-Resolved" } }, "system_health": { diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json index 8194ea37e6d9be..e2df1d8f916329 100644 --- a/homeassistant/components/hassio/translations/zh-Hant.json +++ b/homeassistant/components/hassio/translations/zh-Hant.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "System is currently unhealthy \u7531\u65bc '{reason}' \u7de3\u6545\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u54ea\u88e1\u51fa\u4e86\u554f\u984c\u3001\u53ca\u5982\u4f55\u9032\u884c\u4fee\u6b63\u3002", + "description": "\u7531\u65bc {reason}\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - {reason}" }, + "unhealthy_docker": { + "description": "\u7531\u65bc Docker \u672a\u6b63\u78ba\u8a2d\u5b9a\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - Docker \u8a2d\u5b9a\u932f\u8aa4" + }, + "unhealthy_privileged": { + "description": "\u7531\u65bc docker runtime \u672a\u7372\u5f97\u5b58\u53d6\u6b0a\u9650\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - \u672a\u53d6\u5f97\u6b0a\u9650" + }, + "unhealthy_setup": { + "description": "\u7531\u65bc\u8a2d\u5b9a\u5931\u6557\u7121\u6cd5\u5b8c\u6210\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - \u8a2d\u5b9a\u5931\u6557" + }, + "unhealthy_supervisor": { + "description": "\u7531\u65bc\u8a66\u5716\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Supervisor \u5931\u6557\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - Supervisor \u66f4\u65b0\u5931\u6557" + }, + "unhealthy_untrusted": { + "description": "\u7531\u65bc\u767c\u73fe\u4f7f\u4f7f\u7528\u4e0d\u53d7\u4fe1\u4efb\u7684\u7a0b\u5f0f\u78bc\u6216\u5716\u50cf\u3001\u7cfb\u7d71\u76ee\u524d\u88ab\u8a8d\u70ba\u4e0d\u5065\u5eb7\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u5065\u5eb7\u7cfb\u7d71 - \u4e0d\u53d7\u4fe1\u4efb\u4e4b\u7a0b\u5f0f\u78bc" + }, "unsupported": { - "description": "System is unsupported \u7531\u65bc '{reason}' \u7de3\u6545\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u76f8\u95dc\u8aaa\u660e\u3001\u53ca\u5982\u4f55\u56de\u5fa9\u81f3\u652f\u63f4\u7cfb\u7d71\u3002", + "description": "\u7531\u65bc {reason}\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u8003\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - {reason}" + }, + "unsupported_apparmor": { + "description": "\u7531\u65bc AppArmor \u672a\u6b63\u5e38\u5de5\u4f5c\u3001\u9644\u52a0\u5143\u4ef6\u4ee5\u672a\u53d7\u4fdd\u8b77\u53ca\u4e0d\u5b89\u5168\u65b9\u5f0f\u57f7\u884c\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - AppArmor \u554f\u984c" + }, + "unsupported_cgroup_version": { + "description": "\u7531\u65bc\u4f7f\u7528\u932f\u8aa4\u7248\u672c\u4e4b Docker CGroup\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - CGroup \u7248\u672c" + }, + "unsupported_connectivity_check": { + "description": "\u7531\u65bc Home Assistant \u7121\u6cd5\u78ba\u5b9a\u7db2\u969b\u7db2\u8def\u9023\u7dda\u662f\u5426\u53ef\u7528\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u9023\u7dda\u6aa2\u67e5\u5df2\u95dc\u9589" + }, + "unsupported_content_trust": { + "description": "\u7531\u65bc Home Assistant \u7121\u6cd5\u9a57\u8b49\u57f7\u884c\u7684\u70ba\u4fe1\u4efb\u5167\u5bb9\uff0c\u672a\u53d7\u5230\u653b\u64ca\u8005\u4fee\u6539\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u5167\u5bb9\u4fe1\u4efb\u6aa2\u67e5\u5df2\u95dc\u9589" + }, + "unsupported_dbus": { + "description": "System is unsupported \u7531\u65bc D-Bus \u672a\u6b63\u5e38\u5de5\u4f5c\u3001\u7cfb\u7d71\u4e0d\u5065\u5eb7\u3002\u7f3a\u4e4f\u6b64\u529f\u80fd\u3001Supervisor \u5c07\u7121\u6cd5\u6b63\u5e38\u8207\u4e3b\u6a5f\u9032\u884c\u901a\u8a0a\u3001\u8a31\u591a\u529f\u80fd\u7686\u7121\u6cd5\u6b63\u5e38\u57f7\u884c\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - D-Bus \u554f\u984c" + }, + "unsupported_dns_server": { + "description": "\u7531\u65bc\u63d0\u4f9b\u7684 DNS \u4f3a\u670d\u5668\u672a\u6b63\u5e38\u5de5\u4f5c\u3001fallback DNS \u9078\u9805\u5df2\u7d93\u95dc\u9589\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - DNS \u4f3a\u670d\u5668\u554f\u984c" + }, + "unsupported_docker_configuration": { + "description": "\u7531\u65bc Docker daemon \u6b63\u4ee5\u672a\u9810\u671f\u65b9\u5f0f\u57f7\u884c\u4e2d\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Docker \u8a2d\u5b9a\u932f\u8aa4" + }, + "unsupported_docker_version": { + "description": "System is unsupported \u7531\u65bc\u4f7f\u7528\u932f\u8aa4\u7248\u672c\u4e4b Docker\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Docker \u7248\u672c" + }, + "unsupported_job_conditions": { + "description": "\u7531\u65bc\u4e00\u500b\u6216\u4ee5\u4e0a\u4fdd\u8b77\u672a\u9810\u671f\u5931\u6557\u6216\u640d\u6bc0\u7684\u689d\u4ef6\u5df2\u906d\u5230\u95dc\u9589\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u4fdd\u8b77\u5df2\u95dc\u9589" + }, + "unsupported_lxc": { + "description": "\u7531\u65bc\u7cfb\u7d71\u6b63\u4ee5 LXC \u865b\u64ec\u6a5f\u57f7\u884c\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u5075\u6e2c\u5230 LXC" + }, + "unsupported_network_manager": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528\u7db2\u8def\u7ba1\u7406\u54e1\uff0c\u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u7db2\u8def\u7ba1\u7406\u54e1\u554f\u984c" + }, + "unsupported_os": { + "description": "\u7531\u65bc\u4f5c\u696d\u7cfb\u7d71\u672a\u91dd\u5c0d\u4f7f\u7528 Supervisor \u9032\u884c\u904e\u6e2c\u8a66\u6216\u7dad\u8b77\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u4f5c\u696d\u7cfb\u7d71" + }, + "unsupported_os_agent": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528 OS-Agent \u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - OS-Agent \u554f\u984c" + }, + "unsupported_restart_policy": { + "description": "\u7531\u65bc Docker container \u8a2d\u5b9a\u4e86\u91cd\u555f\u653f\u7b56\uff0c\u53ef\u80fd\u6703\u5c0e\u81f4\u555f\u52d5\u554f\u984c\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Container \u91cd\u555f\u653f\u7b56" + }, + "unsupported_software": { + "description": "\u7531\u65bc\u5075\u6e2c\u5230 Home Assistant \u751f\u614b\u7cfb\u7d71\u4e4b\u5916\u7684\u9644\u52a0\u8edf\u9ad4\u7248\u672c\u904e\u820a\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - \u4e0d\u652f\u63f4\u8edf\u9ad4" + }, + "unsupported_source_mods": { + "description": "\u7531\u65bc Supervisor \u4f86\u6e90\u78bc\u906d\u5230\u4fee\u6539\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Supervisor \u4f86\u6e90\u4fee\u6539" + }, + "unsupported_supervisor_version": { + "description": "\u7531\u65bc\u6240\u4f7f\u7528\u7684 Supervisor \u7248\u672c\u904e\u820a\u3001\u4e26\u4e14\u5df2\u95dc\u9589\u81ea\u52d5\u66f4\u65b0\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3001\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Supervisor \u7248\u672c" + }, + "unsupported_systemd": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528 Systemd \u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u591a\u8a73\u7d30\u8cc7\u8a0a\uff0c\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Systemd \u554f\u984c" + }, + "unsupported_systemd_journal": { + "description": "\u7531\u65bc Systemd \u65e5\u8a8c\u53ca/\u6216\u7f3a\u5c11\u3001\u672a\u555f\u7528\u9598\u9053\u5668\u6216\u8a2d\u5b9a\u932f\u8aa4\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002 \u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u591a\u8a73\u7d30\u8cc7\u8a0a\uff0c\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Systemd \u65e5\u8a8c\u554f\u984c" + }, + "unsupported_systemd_resolved": { + "description": "\u7531\u65bc\u7f3a\u5c11\u3001\u672a\u555f\u7528 Systemd \u672c\u6a5f\u89e3\u6790\u6216\u8a2d\u5b9a\u932f\u8aa4\u7684\u3001\u7cfb\u7d71\u4e0d\u652f\u63f4\u3002\u8acb\u53c3\u95b1\u9023\u7d50\u4ee5\u4e86\u89e3\u66f4\u591a\u8a73\u7d30\u8cc7\u8a0a\uff0c\u53ca\u5982\u4f55\u4fee\u6b63\u554f\u984c\u3002", + "title": "\u4e0d\u652f\u63f4\u7cfb\u7d71 - Systemd \u672c\u6a5f\u89e3\u6790\u554f\u984c" } }, "system_health": { diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 4cb0fda8a9108e..08b9c85b110e2f 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -5,15 +5,33 @@ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { - "cannot_connect": "Gagal terhubung" + "bad_birth": "Topik birth tidak valid", + "bad_certificate": "Sertifikat CA tidak valid", + "bad_client_cert": "Sertifikat klien tidak valid, pastikan file dengan format PEM diberikan", + "bad_client_cert_key": "Sertifikat klien dan pribadi bukan pasangan yang valid", + "bad_client_key": "Kunci pribadi tidak valid, pastikan file dengan format PEM diberikan tanpa kata sandi", + "bad_discovery_prefix": "Prefiks topik penemuan tidak valid", + "bad_will": "Topik will tidak valid", + "cannot_connect": "Gagal terhubung", + "invalid_inclusion": "Sertifikat klien dan kunci pribadi harus dikonfigurasi bersama" }, "step": { "broker": { "data": { + "advanced_options": "Opsi tingkat lanjut", "broker": "Broker", + "certificate": "Jalur ke file sertifikat CA khusus", + "client_cert": "Jalur ke file sertifikat klien", + "client_id": "ID Klien (biarkan kosong agar dihasilkan secara acak)", + "client_key": "Jalur ke file kunci pribadi", "discovery": "Aktifkan penemuan", + "keepalive": "Waktu antara mengirim pesan tetap hidup", "password": "Kata Sandi", "port": "Port", + "protocol": "Protokol MQTT", + "set_ca_cert": "Validasi sertifikat broker", + "set_client_cert": "Gunakan sertifikat klien", + "tls_insecure": "Abaikan validasi sertifikat broker", "username": "Nama Pengguna" }, "description": "Masukkan informasi koneksi broker MQTT Anda." @@ -53,18 +71,30 @@ "deprecated_yaml": { "description": "MQTT {platform} yang dikonfigurasi secara manual ditemukan di bawah kunci platform `{platform}`.\n\nPindahkan konfigurasi ke kunci integrasi `mqtt` dan mulai ulang Home Assistant untuk memperbaiki masalah ini. Lihat [dokumentasi]({more_info_url}), untuk informasi lebih lanjut.", "title": "Entitas MQTT {platform} yang dikonfigurasi secara manual membutuhkan perhatian" + }, + "deprecated_yaml_broker_settings": { + "title": "Pengaturan MQTT yang usang ditemukan di `configuration.yaml`" } }, "options": { "error": { "bad_birth": "Topik birth tidak valid", + "bad_certificate": "Sertifikat CA tidak valid", + "bad_client_cert": "Sertifikat klien tidak valid, pastikan file dengan format PEM diberikan", + "bad_client_cert_key": "Sertifikat klien dan pribadi bukan pasangan yang valid", + "bad_client_key": "Kunci pribadi tidak valid, pastikan file dengan format PEM diberikan tanpa kata sandi", + "bad_discovery_prefix": "Prefiks topik penemuan tidak valid", "bad_will": "Topik will tidak valid", - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_inclusion": "Sertifikat klien dan kunci pribadi harus dikonfigurasi bersama" }, "step": { "broker": { "data": { + "advanced_options": "Opsi tingkat lanjut", "broker": "Broker", + "certificate": "Unggah file sertifikat CA khusus", + "client_cert": "Unggah file sertifikat klien", "password": "Kata Sandi", "port": "Port", "username": "Nama Pengguna" diff --git a/homeassistant/components/pushbullet/translations/ca.json b/homeassistant/components/pushbullet/translations/ca.json new file mode 100644 index 00000000000000..a9ed110f004f97 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "name": "Nom" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Pushbullet mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Pushbullet del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Pushbullet est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/de.json b/homeassistant/components/pushbullet/translations/de.json new file mode 100644 index 00000000000000..7e7fa2f0e1458e --- /dev/null +++ b/homeassistant/components/pushbullet/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "name": "Name" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Pushbullet mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Pushbullet-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Pushbullet-YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/es.json b/homeassistant/components/pushbullet/translations/es.json new file mode 100644 index 00000000000000..4321936dbca37a --- /dev/null +++ b/homeassistant/components/pushbullet/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "name": "Nombre" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Pushbullet mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Pushbullet de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "La configuraci\u00f3n YAML de Pushbullet se va a eliminar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/hu.json b/homeassistant/components/pushbullet/translations/hu.json new file mode 100644 index 00000000000000..09325e78e70fd2 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "name": "Elnevez\u00e9s" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Pushbullet konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Pushbullet YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Pushbullet YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/it.json b/homeassistant/components/pushbullet/translations/it.json new file mode 100644 index 00000000000000..94911243672f4c --- /dev/null +++ b/homeassistant/components/pushbullet/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "name": "Nome" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Pushbullet tramite YAML \u00e8 stata rimossa. \n\nLa tua configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovere la configurazione YAML di Pushbullet dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Pushbullet \u00e8 stata rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/pt-BR.json b/homeassistant/components/pushbullet/translations/pt-BR.json new file mode 100644 index 00000000000000..16a928353cda28 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave de API", + "name": "Nome" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Pushbullet usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Pushbullet do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Pushbullet est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/ru.json b/homeassistant/components/pushbullet/translations/ru.json new file mode 100644 index 00000000000000..815e732f7dd97b --- /dev/null +++ b/homeassistant/components/pushbullet/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Pushbullet \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \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. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \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 Pushbullet \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file From 28989754cda667d0828416d41ef4eb1a3a707ebb Mon Sep 17 00:00:00 2001 From: Austin Brunkhorst Date: Wed, 2 Nov 2022 19:10:07 -0700 Subject: [PATCH 199/394] Update pysnooz to 0.8.3 (#81428) --- homeassistant/components/snooz/config_flow.py | 2 +- homeassistant/components/snooz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/snooz/{test_config.py => test_init.py} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename tests/components/snooz/{test_config.py => test_init.py} (100%) diff --git a/homeassistant/components/snooz/config_flow.py b/homeassistant/components/snooz/config_flow.py index 48f9370e403d21..eb05edcbefaec7 100644 --- a/homeassistant/components/snooz/config_flow.py +++ b/homeassistant/components/snooz/config_flow.py @@ -82,7 +82,7 @@ async def async_step_user( if user_input is not None: name = user_input[CONF_NAME] - discovered = self._discovered_devices.get(name) + discovered = self._discovered_devices[name] assert discovered is not None diff --git a/homeassistant/components/snooz/manifest.json b/homeassistant/components/snooz/manifest.json index 1384767e8b8a35..91185bcd5b2fa1 100644 --- a/homeassistant/components/snooz/manifest.json +++ b/homeassistant/components/snooz/manifest.json @@ -3,7 +3,7 @@ "name": "Snooz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/snooz", - "requirements": ["pysnooz==0.8.2"], + "requirements": ["pysnooz==0.8.3"], "dependencies": ["bluetooth"], "codeowners": ["@AustinBrunkhorst"], "bluetooth": [ diff --git a/requirements_all.txt b/requirements_all.txt index a3a177edef5d2b..e5973baf0cb17d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1914,7 +1914,7 @@ pysml==0.0.8 pysnmplib==5.0.15 # homeassistant.components.snooz -pysnooz==0.8.2 +pysnooz==0.8.3 # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8ceeb06fbd29f..8dd8d3592fcf3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1349,7 +1349,7 @@ pysmartthings==0.7.6 pysnmplib==5.0.15 # homeassistant.components.snooz -pysnooz==0.8.2 +pysnooz==0.8.3 # homeassistant.components.soma pysoma==0.0.10 diff --git a/tests/components/snooz/test_config.py b/tests/components/snooz/test_init.py similarity index 100% rename from tests/components/snooz/test_config.py rename to tests/components/snooz/test_init.py From 6bd8cf00729aff12978e97115182acfbc66db54d Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 3 Nov 2022 08:27:46 +0100 Subject: [PATCH 200/394] Use 'kWh' as unit for 'IEC_ENERGY_COUNTER' (#81427) The standard unit for the 'IEC_ENERGY_COUNTER' type is 'kWh' instead of 'Wh' --- homeassistant/components/homematic/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index c7a78c7bbcffe8..806d56b2c1dba8 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -17,6 +17,7 @@ DEGREE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, FREQUENCY_HERTZ, LENGTH_MILLIMETERS, @@ -141,7 +142,7 @@ ), "IEC_ENERGY_COUNTER": SensorEntityDescription( key="IEC_ENERGY_COUNTER", - native_unit_of_measurement=ENERGY_WATT_HOUR, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), From adf35e5ec2019dae10954283dd0ab6ba82f2b026 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 08:51:08 +0100 Subject: [PATCH 201/394] Skip flume devices with location missing (#81441) fixes #81438 --- homeassistant/components/flume/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flume/util.py b/homeassistant/components/flume/util.py index b943124b877c5e..58b3920c9be895 100644 --- a/homeassistant/components/flume/util.py +++ b/homeassistant/components/flume/util.py @@ -14,5 +14,6 @@ def get_valid_flume_devices(flume_devices: FlumeDeviceList) -> list[dict[str, An return [ device for device in flume_devices.device_list - if KEY_DEVICE_LOCATION_NAME in device[KEY_DEVICE_LOCATION] + if KEY_DEVICE_LOCATION in device + and KEY_DEVICE_LOCATION_NAME in device[KEY_DEVICE_LOCATION] ] From ee4d28000da5d59b7162cb434d210bafa72cdbc9 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 3 Nov 2022 19:32:40 +1100 Subject: [PATCH 202/394] Add integration_type to gdacs (#81451) --- homeassistant/components/gdacs/manifest.json | 3 ++- homeassistant/generated/integrations.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gdacs/manifest.json b/homeassistant/components/gdacs/manifest.json index 57c275f2beb084..b378368a326487 100644 --- a/homeassistant/components/gdacs/manifest.json +++ b/homeassistant/components/gdacs/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aio_georss_gdacs==0.7"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "integration_type": "service" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index a955193624c785..1d04bde132ab7e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1773,7 +1773,7 @@ }, "gdacs": { "name": "Global Disaster Alert and Coordination System (GDACS)", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, From 739ed6a6c86fed85de61562bb20344adc66a4ed6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 3 Nov 2022 04:43:48 -0400 Subject: [PATCH 203/394] Fix eight sleep client creation (#81440) Fix eight sleep bug --- homeassistant/components/eight_sleep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 67ff6c59a542cc..2642505fbea82f 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -95,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], hass.config.time_zone, - async_get_clientsession(hass), + client_session=async_get_clientsession(hass), ) # Authenticate, build sensors From 328eda044a46dd69d46400139976554b6911fad0 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 3 Nov 2022 11:02:25 +0200 Subject: [PATCH 204/394] Use DataUpdateCoordinator for glances (#72748) * use DataUpdateCoordinator for glances add tests to increase coverage fix test_config_flow.py fix codecov/patch remove unused const, minor tweaks remove invalid_auth test as it is not implemented fix type hints * change to async_forward_entry_setups * Use Dataupdatecoordinator for glances * minor fixex * minor fixes * minor fix * remove support_versions const * coe cleanup * address comments * fix sensor native_value * Rename entry to entry_data in `get_api` * Remove whitespace in sensor name --- .coveragerc | 2 +- homeassistant/components/glances/__init__.py | 127 ++----------- .../components/glances/config_flow.py | 48 +---- homeassistant/components/glances/const.py | 1 - .../components/glances/coordinator.py | 42 +++++ homeassistant/components/glances/sensor.py | 168 ++++++++---------- homeassistant/components/glances/strings.json | 13 +- .../components/glances/translations/en.json | 13 +- tests/components/glances/__init__.py | 41 +++++ tests/components/glances/conftest.py | 15 ++ tests/components/glances/test_config_flow.py | 101 +++-------- tests/components/glances/test_init.py | 49 +++++ 12 files changed, 280 insertions(+), 340 deletions(-) create mode 100644 homeassistant/components/glances/coordinator.py create mode 100644 tests/components/glances/conftest.py create mode 100644 tests/components/glances/test_init.py diff --git a/.coveragerc b/.coveragerc index b782f8444dbdcc..78168feced05c4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -455,7 +455,7 @@ omit = homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py - homeassistant/components/glances/__init__.py + homeassistant/components/glances/const.py homeassistant/components/glances/sensor.py homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 0747db89cd26fb..bda1baf797af55 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -1,27 +1,16 @@ """The Glances component.""" -from datetime import timedelta -import logging +from typing import Any -from glances_api import Glances, exceptions +from glances_api import Glances from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_VERIFY_SSL, - Platform, -) +from homeassistant.const import CONF_NAME, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.httpx_client import get_async_client -from .const import DATA_UPDATED, DEFAULT_SCAN_INTERVAL, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN +from .coordinator import GlancesDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] @@ -30,106 +19,28 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Glances from config entry.""" - client = GlancesData(hass, config_entry) - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client - if not await client.async_setup(): - return False + api = get_api(hass, dict(config_entry.data)) + coordinator = GlancesDataUpdateCoordinator(hass, config_entry, api) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] return unload_ok -class GlancesData: - """Get the latest data from Glances api.""" - - def __init__(self, hass, config_entry): - """Initialize the Glances data.""" - self.hass = hass - self.config_entry = config_entry - self.api = None - self.unsub_timer = None - self.available = False - - @property - def host(self): - """Return client host.""" - return self.config_entry.data[CONF_HOST] - - async def async_update(self): - """Get the latest data from the Glances REST API.""" - try: - await self.api.get_data("all") - self.available = True - except exceptions.GlancesApiError: - _LOGGER.error("Unable to fetch data from Glances") - self.available = False - _LOGGER.debug("Glances data updated") - async_dispatcher_send(self.hass, DATA_UPDATED) - - async def async_setup(self): - """Set up the Glances client.""" - try: - self.api = get_api(self.hass, self.config_entry.data) - await self.api.get_data("all") - self.available = True - _LOGGER.debug("Successfully connected to Glances") - - except exceptions.GlancesApiConnectionError as err: - _LOGGER.debug("Can not connect to Glances") - raise ConfigEntryNotReady from err - - self.add_options() - self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - self.config_entry.async_on_unload( - self.config_entry.add_update_listener(self.async_options_updated) - ) - - await self.hass.config_entries.async_forward_entry_setups( - self.config_entry, PLATFORMS - ) - - return True - - def add_options(self): - """Add options for Glances integration.""" - if not self.config_entry.options: - options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} - self.hass.config_entries.async_update_entry( - self.config_entry, options=options - ) - - def set_scan_interval(self, scan_interval): - """Update scan interval.""" - - async def refresh(event_time): - """Get the latest data from Glances api.""" - await self.async_update() - - if self.unsub_timer is not None: - self.unsub_timer() - self.unsub_timer = async_track_time_interval( - self.hass, refresh, timedelta(seconds=scan_interval) - ) - - @staticmethod - async def async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Triggered by config entry options updates.""" - hass.data[DOMAIN][entry.entry_id].set_scan_interval( - entry.options[CONF_SCAN_INTERVAL] - ) - - -def get_api(hass, entry): +def get_api(hass: HomeAssistant, entry_data: dict[str, Any]) -> Glances: """Return the api from glances_api.""" - params = entry.copy() - params.pop(CONF_NAME, None) - verify_ssl = params.pop(CONF_VERIFY_SSL, True) - httpx_client = get_async_client(hass, verify_ssl=verify_ssl) - return Glances(httpx_client=httpx_client, **params) + entry_data.pop(CONF_NAME, None) + httpx_client = get_async_client(hass, verify_ssl=entry_data[CONF_VERIFY_SSL]) + return Glances(httpx_client=httpx_client, **entry_data) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index a56fa795491cfb..cf55118a913cb5 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -3,20 +3,19 @@ from typing import Any -import glances_api +from glances_api.exceptions import GlancesApiError import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from . import get_api @@ -24,7 +23,6 @@ CONF_VERSION, DEFAULT_HOST, DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, DEFAULT_VERSION, DOMAIN, SUPPORTED_VERSIONS, @@ -43,12 +41,12 @@ ) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: """Validate the user input allows us to connect.""" + api = get_api(hass, data) try: - api = get_api(hass, data) await api.get_data("all") - except glances_api.exceptions.GlancesApiConnectionError as err: + except GlancesApiError as err: raise CannotConnect from err @@ -57,14 +55,6 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - @staticmethod - @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> GlancesOptionsFlowHandler: - """Get the options flow for this handler.""" - return GlancesOptionsFlowHandler(config_entry) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -85,31 +75,5 @@ async def async_step_user( ) -class GlancesOptionsFlowHandler(config_entries.OptionsFlow): - """Handle Glances client options.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize Glances options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the Glances options.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - options = { - vol.Optional( - CONF_SCAN_INTERVAL, - default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ), - ): int - } - - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) - - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index efcc30c057b99d..b704ab326f41be 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -10,7 +10,6 @@ DEFAULT_VERSION = 3 DEFAULT_SCAN_INTERVAL = 60 -DATA_UPDATED = "glances_data_updated" SUPPORTED_VERSIONS = [2, 3] CPU_ICON = f"mdi:cpu-{64 if sys.maxsize > 2**32 else 32}-bit" diff --git a/homeassistant/components/glances/coordinator.py b/homeassistant/components/glances/coordinator.py new file mode 100644 index 00000000000000..8ffd2a2da6e108 --- /dev/null +++ b/homeassistant/components/glances/coordinator.py @@ -0,0 +1,42 @@ +"""Coordinator for Glances integration.""" +from datetime import timedelta +import logging +from typing import Any + +from glances_api import Glances, exceptions + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Get the latest data from Glances api.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry, api: Glances) -> None: + """Initialize the Glances data.""" + self.hass = hass + self.config_entry = entry + self.host: str = entry.data[CONF_HOST] + self.api = api + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN} - {self.host}", + update_interval=timedelta(seconds=60), + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Get the latest data from the Glances REST API.""" + try: + await self.api.get_data("all") + except exceptions.GlancesApiError as err: + raise UpdateFailed from err + return self.api.data diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 13f4284acd3734..a479cb260de7ac 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -8,10 +8,10 @@ SensorEntity, SensorEntityDescription, SensorStateClass, + StateType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_HOST, CONF_NAME, DATA_GIBIBYTES, DATA_MEBIBYTES, @@ -21,22 +21,29 @@ TEMP_CELSIUS, Platform, ) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import GlancesData -from .const import CPU_ICON, DATA_UPDATED, DOMAIN +from . import GlancesDataUpdateCoordinator +from .const import CPU_ICON, DOMAIN @dataclass -class GlancesSensorEntityDescription(SensorEntityDescription): - """Describe Glances sensor entity.""" +class GlancesSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + type: str + name_suffix: str - type: str | None = None - name_suffix: str | None = None + +@dataclass +class GlancesSensorEntityDescription( + SensorEntityDescription, GlancesSensorEntityDescriptionMixin +): + """Describe Glances sensor entity.""" SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( @@ -234,9 +241,9 @@ async def async_setup_entry( ) -> None: """Set up the Glances sensors.""" - client: GlancesData = hass.data[DOMAIN][config_entry.entry_id] + coordinator: GlancesDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data.get(CONF_NAME) - dev = [] + entities = [] @callback def _migrate_old_unique_ids( @@ -256,15 +263,15 @@ def _migrate_old_unique_ids( for description in SENSOR_TYPES: if description.type == "fs": # fs will provide a list of disks attached - for disk in client.api.data[description.type]: + for disk in coordinator.data[description.type]: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {disk['mnt_point']} {description.name_suffix}", + f"{coordinator.host}-{name} {disk['mnt_point']} {description.name_suffix}", f"{disk['mnt_point']}-{description.key}", ) - dev.append( + entities.append( GlancesSensor( - client, + coordinator, name, disk["mnt_point"], description, @@ -272,101 +279,80 @@ def _migrate_old_unique_ids( ) elif description.type == "sensors": # sensors will provide temp for different devices - for sensor in client.api.data[description.type]: + for sensor in coordinator.data[description.type]: if sensor["type"] == description.key: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {sensor['label']} {description.name_suffix}", + f"{coordinator.host}-{name} {sensor['label']} {description.name_suffix}", f"{sensor['label']}-{description.key}", ) - dev.append( + entities.append( GlancesSensor( - client, + coordinator, name, sensor["label"], description, ) ) elif description.type == "raid": - for raid_device in client.api.data[description.type]: + for raid_device in coordinator.data[description.type]: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {raid_device} {description.name_suffix}", + f"{coordinator.host}-{name} {raid_device} {description.name_suffix}", f"{raid_device}-{description.key}", ) - dev.append(GlancesSensor(client, name, raid_device, description)) - elif client.api.data[description.type]: + entities.append( + GlancesSensor(coordinator, name, raid_device, description) + ) + elif coordinator.data[description.type]: _migrate_old_unique_ids( hass, - f"{client.host}-{name} {description.name_suffix}", + f"{coordinator.host}-{name} {description.name_suffix}", f"-{description.key}", ) - dev.append( + entities.append( GlancesSensor( - client, + coordinator, name, "", description, ) ) - async_add_entities(dev, True) + async_add_entities(entities) -class GlancesSensor(SensorEntity): +class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntity): """Implementation of a Glances sensor.""" entity_description: GlancesSensorEntityDescription _attr_has_entity_name = True - _attr_should_poll = False def __init__( self, - glances_data: GlancesData, + coordinator: GlancesDataUpdateCoordinator, name: str | None, sensor_name_prefix: str, description: GlancesSensorEntityDescription, ) -> None: """Initialize the sensor.""" - self.glances_data = glances_data + super().__init__(coordinator) self._sensor_name_prefix = sensor_name_prefix - self.unsub_update: CALLBACK_TYPE | None = None - self.entity_description = description - self._attr_name = f"{sensor_name_prefix} {description.name_suffix}" + self._attr_name = f"{sensor_name_prefix} {description.name_suffix}".strip() self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, glances_data.config_entry.entry_id)}, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, manufacturer="Glances", - name=name or glances_data.config_entry.data[CONF_HOST], + name=name or coordinator.host, ) - self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @property - def available(self) -> bool: - """Could the device be accessed during the last update call.""" - return self.glances_data.available - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - self.unsub_update = async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update - ) - - @callback - def _schedule_immediate_update(self) -> None: - self.async_schedule_update_ha_state(True) - - async def will_remove_from_hass(self) -> None: - """Unsubscribe from update dispatcher.""" - if self.unsub_update: - self.unsub_update() - self.unsub_update = None - - async def async_update(self) -> None: # noqa: C901 - """Get the latest data from REST API.""" - if (value := self.glances_data.api.data) is None: - return - + def native_value(self) -> StateType: # noqa: C901 + """Return the state of the resources.""" + if (value := self.coordinator.data) is None: + return None + state: StateType = None if self.entity_description.type == "fs": for var in value["fs"]: if var["mnt_point"] == self._sensor_name_prefix: @@ -374,100 +360,102 @@ async def async_update(self) -> None: # noqa: C901 break if self.entity_description.key == "disk_free": try: - self._attr_native_value = round(disk["free"] / 1024**3, 1) + state = round(disk["free"] / 1024**3, 1) except KeyError: - self._attr_native_value = round( + state = round( (disk["size"] - disk["used"]) / 1024**3, 1, ) elif self.entity_description.key == "disk_use": - self._attr_native_value = round(disk["used"] / 1024**3, 1) + state = round(disk["used"] / 1024**3, 1) elif self.entity_description.key == "disk_use_percent": - self._attr_native_value = disk["percent"] + state = disk["percent"] elif self.entity_description.key == "battery": for sensor in value["sensors"]: if ( sensor["type"] == "battery" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "fan_speed": for sensor in value["sensors"]: if ( sensor["type"] == "fan_speed" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "temperature_core": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_core" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "temperature_hdd": for sensor in value["sensors"]: if ( sensor["type"] == "temperature_hdd" and sensor["label"] == self._sensor_name_prefix ): - self._attr_native_value = sensor["value"] + state = sensor["value"] elif self.entity_description.key == "memory_use_percent": - self._attr_native_value = value["mem"]["percent"] + state = value["mem"]["percent"] elif self.entity_description.key == "memory_use": - self._attr_native_value = round(value["mem"]["used"] / 1024**2, 1) + state = round(value["mem"]["used"] / 1024**2, 1) elif self.entity_description.key == "memory_free": - self._attr_native_value = round(value["mem"]["free"] / 1024**2, 1) + state = round(value["mem"]["free"] / 1024**2, 1) elif self.entity_description.key == "swap_use_percent": - self._attr_native_value = value["memswap"]["percent"] + state = value["memswap"]["percent"] elif self.entity_description.key == "swap_use": - self._attr_native_value = round(value["memswap"]["used"] / 1024**3, 1) + state = round(value["memswap"]["used"] / 1024**3, 1) elif self.entity_description.key == "swap_free": - self._attr_native_value = round(value["memswap"]["free"] / 1024**3, 1) + state = round(value["memswap"]["free"] / 1024**3, 1) elif self.entity_description.key == "processor_load": # Windows systems don't provide load details try: - self._attr_native_value = value["load"]["min15"] + state = value["load"]["min15"] except KeyError: - self._attr_native_value = value["cpu"]["total"] + state = value["cpu"]["total"] elif self.entity_description.key == "process_running": - self._attr_native_value = value["processcount"]["running"] + state = value["processcount"]["running"] elif self.entity_description.key == "process_total": - self._attr_native_value = value["processcount"]["total"] + state = value["processcount"]["total"] elif self.entity_description.key == "process_thread": - self._attr_native_value = value["processcount"]["thread"] + state = value["processcount"]["thread"] elif self.entity_description.key == "process_sleeping": - self._attr_native_value = value["processcount"]["sleeping"] + state = value["processcount"]["sleeping"] elif self.entity_description.key == "cpu_use_percent": - self._attr_native_value = value["quicklook"]["cpu"] + state = value["quicklook"]["cpu"] elif self.entity_description.key == "docker_active": count = 0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: count += 1 - self._attr_native_value = count + state = count except KeyError: - self._attr_native_value = count + state = count elif self.entity_description.key == "docker_cpu_use": cpu_use = 0.0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: cpu_use += container["cpu"]["total"] - self._attr_native_value = round(cpu_use, 1) + state = round(cpu_use, 1) except KeyError: - self._attr_native_value = STATE_UNAVAILABLE + state = STATE_UNAVAILABLE elif self.entity_description.key == "docker_memory_use": mem_use = 0.0 try: for container in value["docker"]["containers"]: if container["Status"] == "running" or "Up" in container["Status"]: mem_use += container["memory"]["usage"] - self._attr_native_value = round(mem_use / 1024**2, 1) + state = round(mem_use / 1024**2, 1) except KeyError: - self._attr_native_value = STATE_UNAVAILABLE + state = STATE_UNAVAILABLE elif self.entity_description.type == "raid": for raid_device, raid in value["raid"].items(): if raid_device == self._sensor_name_prefix: - self._attr_native_value = raid[self.entity_description.key] + state = raid[self.entity_description.key] + + return state diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index 11c9792f364281..b46716b43c0a27 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -14,21 +14,10 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "wrong_version": "Version not supported (2 or 3 only)" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } - }, - "options": { - "step": { - "init": { - "description": "Configure options for Glances", - "data": { - "scan_interval": "Update frequency" - } - } - } } } diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json index 87c53c3cf48a2f..726e4716224580 100644 --- a/homeassistant/components/glances/translations/en.json +++ b/homeassistant/components/glances/translations/en.json @@ -4,8 +4,7 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect", - "wrong_version": "Version not supported (2 or 3 only)" + "cannot_connect": "Failed to connect" }, "step": { "user": { @@ -22,15 +21,5 @@ "title": "Setup Glances" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Update frequency" - }, - "description": "Configure options for Glances" - } - } } } \ No newline at end of file diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py index 488265f970b323..4818e9258de438 100644 --- a/tests/components/glances/__init__.py +++ b/tests/components/glances/__init__.py @@ -1 +1,42 @@ """Tests for Glances.""" + +MOCK_USER_INPUT = { + "host": "0.0.0.0", + "username": "username", + "password": "password", + "version": 3, + "port": 61208, + "ssl": False, + "verify_ssl": True, +} + +MOCK_DATA = { + "cpu": { + "total": 10.6, + "user": 7.6, + "system": 2.1, + "idle": 88.8, + "nice": 0.0, + "iowait": 0.6, + }, + "diskio": [ + { + "time_since_update": 1, + "disk_name": "nvme0n1", + "read_count": 12, + "write_count": 466, + "read_bytes": 184320, + "write_bytes": 23863296, + "key": "disk_name", + }, + ], + "system": { + "os_name": "Linux", + "hostname": "fedora-35", + "platform": "64bit", + "linux_distro": "Fedora Linux 35", + "os_version": "5.15.6-200.fc35.x86_64", + "hr_name": "Fedora Linux 35 64bit", + }, + "uptime": "3 days, 10:25:20", +} diff --git a/tests/components/glances/conftest.py b/tests/components/glances/conftest.py new file mode 100644 index 00000000000000..d92d3cc33d4f45 --- /dev/null +++ b/tests/components/glances/conftest.py @@ -0,0 +1,15 @@ +"""Conftest for speedtestdotnet.""" +from unittest.mock import AsyncMock, patch + +import pytest + +from . import MOCK_DATA + + +@pytest.fixture(autouse=True) +def mock_api(): + """Mock glances api.""" + with patch("homeassistant.components.glances.Glances") as mock_api: + mock_api.return_value.get_data = AsyncMock(return_value=None) + mock_api.return_value.data.return_value = MOCK_DATA + yield mock_api diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 40e40b45e117d2..ab6420550593f5 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -1,38 +1,22 @@ """Tests for Glances config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock -from glances_api import exceptions +from glances_api.exceptions import GlancesApiConnectionError import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import glances -from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType -from tests.common import MockConfigEntry +from . import MOCK_USER_INPUT -NAME = "Glances" -HOST = "0.0.0.0" -USERNAME = "username" -PASSWORD = "password" -PORT = 61208 -VERSION = 3 -SCAN_INTERVAL = 10 - -DEMO_USER_INPUT = { - "host": HOST, - "username": USERNAME, - "password": PASSWORD, - "version": VERSION, - "port": PORT, - "ssl": False, - "verify_ssl": True, -} +from tests.common import MockConfigEntry, patch @pytest.fixture(autouse=True) def glances_setup_fixture(): - """Mock transmission entry setup.""" + """Mock glances entry setup.""" with patch("homeassistant.components.glances.async_setup_entry", return_value=True): yield @@ -43,74 +27,43 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - with patch("homeassistant.components.glances.Glances.get_data", autospec=True): - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_USER_INPUT - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_INPUT + ) - assert result["type"] == "create_entry" - assert result["title"] == HOST - assert result["data"] == DEMO_USER_INPUT + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "0.0.0.0" + assert result["data"] == MOCK_USER_INPUT -async def test_form_cannot_connect(hass: HomeAssistant) -> None: +async def test_form_cannot_connect(hass: HomeAssistant, mock_api: MagicMock) -> None: """Test to return error if we cannot connect.""" - with patch( - "homeassistant.components.glances.Glances.get_data", - side_effect=exceptions.GlancesApiConnectionError, - ): - result = await hass.config_entries.flow.async_init( - glances.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_USER_INPUT - ) - - assert result["type"] == "form" + mock_api.return_value.get_data.side_effect = GlancesApiConnectionError + result = await hass.config_entries.flow.async_init( + glances.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_INPUT + ) + + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} async def test_form_already_configured(hass: HomeAssistant) -> None: """Test host is already configured.""" - entry = MockConfigEntry( - domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} - ) + entry = MockConfigEntry(domain=glances.DOMAIN, data=MOCK_USER_INPUT) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=DEMO_USER_INPUT + result["flow_id"], user_input=MOCK_USER_INPUT ) - assert result["type"] == "abort" + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_options(hass: HomeAssistant) -> None: - """Test options for Glances.""" - entry = MockConfigEntry( - domain=glances.DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={glances.CONF_SCAN_INTERVAL: 10} - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == { - glances.CONF_SCAN_INTERVAL: 10, - } diff --git a/tests/components/glances/test_init.py b/tests/components/glances/test_init.py new file mode 100644 index 00000000000000..944d9d55ae211a --- /dev/null +++ b/tests/components/glances/test_init.py @@ -0,0 +1,49 @@ +"""Tests for Glances integration.""" +from unittest.mock import MagicMock + +from glances_api.exceptions import GlancesApiConnectionError + +from homeassistant.components.glances.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_USER_INPUT + +from tests.common import MockConfigEntry + + +async def test_successful_config_entry(hass: HomeAssistant) -> None: + """Test that Glances is configured successfully.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED + + +async def test_conn_error(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test Glances failed due to connection error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + mock_api.return_value.get_data.side_effect = GlancesApiConnectionError + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test removing Glances.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert DOMAIN not in hass.data From 203c83b6f0be1fde43c2c869bd05823026a17d4a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 3 Nov 2022 10:58:37 +0100 Subject: [PATCH 205/394] Use _attr_ in MQTT climate (#81406) * Use _attr_ in MQTT climate * Follow up comment * Do not change code --- homeassistant/components/mqtt/climate.py | 286 ++++++++--------------- 1 file changed, 94 insertions(+), 192 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 9f98fcfebdc077..e46c8e31565fe9 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -374,16 +374,6 @@ class MqttClimate(MqttEntity, ClimateEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the climate device.""" - self._action = None - self._aux = False - self._current_fan_mode = None - self._current_operation = None - self._current_swing_mode = None - self._current_temp = None - self._preset_mode = None - self._target_temp = None - self._target_temp_high = None - self._target_temp_low = None self._topic = None self._value_templates = None self._command_templates = None @@ -399,36 +389,54 @@ def config_schema(): def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._attr_hvac_modes = config[CONF_MODE_LIST] + self._attr_min_temp = config[CONF_TEMP_MIN] + self._attr_max_temp = config[CONF_TEMP_MAX] + self._attr_precision = config.get(CONF_PRECISION, super().precision) + self._attr_fan_modes = config[CONF_FAN_MODE_LIST] + self._attr_swing_modes = config[CONF_SWING_MODE_LIST] + self._attr_target_temperature_step = config[CONF_TEMP_STEP] + self._attr_temperature_unit = config.get( + CONF_TEMPERATURE_UNIT, self.hass.config.units.temperature_unit + ) + self._topic = {key: config.get(key) for key in TOPIC_KEYS} # set to None in non-optimistic mode - self._target_temp = ( - self._current_fan_mode - ) = self._current_operation = self._current_swing_mode = None - self._target_temp_low = None - self._target_temp_high = None + self._attr_target_temperature = None + self._attr_fan_mode = None + self._attr_hvac_mode = None + self._attr_swing_mode = None + self._attr_target_temperature_low = None + self._attr_target_temperature_high = None if self._topic[CONF_TEMP_STATE_TOPIC] is None: - self._target_temp = config[CONF_TEMP_INITIAL] + self._attr_target_temperature = config[CONF_TEMP_INITIAL] if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None: - self._target_temp_low = config[CONF_TEMP_INITIAL] + self._attr_target_temperature_low = config[CONF_TEMP_INITIAL] if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None: - self._target_temp_high = config[CONF_TEMP_INITIAL] + self._attr_target_temperature_high = config[CONF_TEMP_INITIAL] if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: - self._current_fan_mode = FAN_LOW + self._attr_fan_mode = FAN_LOW if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: - self._current_swing_mode = SWING_OFF + self._attr_swing_mode = SWING_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None: - self._current_operation = HVACMode.OFF + self._attr_hvac_mode = HVACMode.OFF self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: - self._preset_modes = config[CONF_PRESET_MODES_LIST] + presets = [] + presets.extend(config[CONF_PRESET_MODES_LIST]) + if presets: + presets.insert(0, PRESET_NONE) + self._attr_preset_modes = presets + self._attr_preset_mode = PRESET_NONE else: - self._preset_modes = [] + self._attr_preset_modes = [] self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config - self._action = None - self._aux = False + self._attr_hvac_action = None + + self._attr_is_aux_heat = False value_templates = {} for key in VALUE_TEMPLATE_KEYS: @@ -455,6 +463,41 @@ def _setup_from_config(self, config): self._command_templates = command_templates + support: int = 0 + if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or ( + self._topic[CONF_TEMP_COMMAND_TOPIC] is not None + ): + support |= ClimateEntityFeature.TARGET_TEMPERATURE + + if (self._topic[CONF_TEMP_LOW_STATE_TOPIC] is not None) or ( + self._topic[CONF_TEMP_LOW_COMMAND_TOPIC] is not None + ): + support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + + if (self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is not None) or ( + self._topic[CONF_TEMP_HIGH_COMMAND_TOPIC] is not None + ): + support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + + if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or ( + self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None + ): + support |= ClimateEntityFeature.FAN_MODE + + if (self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None) or ( + self._topic[CONF_SWING_MODE_COMMAND_TOPIC] is not None + ): + support |= ClimateEntityFeature.SWING_MODE + + if self._feature_preset_mode: + support |= ClimateEntityFeature.PRESET_MODE + + if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or ( + self._topic[CONF_AUX_COMMAND_TOPIC] is not None + ): + support |= ClimateEntityFeature.AUX_HEAT + self._attr_supported_features = support + def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -486,7 +529,7 @@ def handle_action_received(msg): ) return try: - self._action = HVACAction(payload) + self._attr_hvac_action = HVACAction(payload) except ValueError: _LOGGER.warning( "Invalid %s action: %s", @@ -514,7 +557,7 @@ def handle_temperature_received(msg, template_name, attr): def handle_current_temperature_received(msg): """Handle current temperature coming via MQTT.""" handle_temperature_received( - msg, CONF_CURRENT_TEMP_TEMPLATE, "_current_temp" + msg, CONF_CURRENT_TEMP_TEMPLATE, "_attr_current_temperature" ) add_subscription( @@ -525,7 +568,9 @@ def handle_current_temperature_received(msg): @log_messages(self.hass, self.entity_id) def handle_target_temperature_received(msg): """Handle target temperature coming via MQTT.""" - handle_temperature_received(msg, CONF_TEMP_STATE_TEMPLATE, "_target_temp") + handle_temperature_received( + msg, CONF_TEMP_STATE_TEMPLATE, "_attr_target_temperature" + ) add_subscription( topics, CONF_TEMP_STATE_TOPIC, handle_target_temperature_received @@ -536,7 +581,7 @@ def handle_target_temperature_received(msg): def handle_temperature_low_received(msg): """Handle target temperature low coming via MQTT.""" handle_temperature_received( - msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_target_temp_low" + msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_attr_target_temperature_low" ) add_subscription( @@ -548,7 +593,7 @@ def handle_temperature_low_received(msg): def handle_temperature_high_received(msg): """Handle target temperature high coming via MQTT.""" handle_temperature_received( - msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_target_temp_high" + msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_attr_target_temperature_high" ) add_subscription( @@ -571,7 +616,7 @@ def handle_mode_received(msg, template_name, attr, mode_list): def handle_current_mode_received(msg): """Handle receiving mode via MQTT.""" handle_mode_received( - msg, CONF_MODE_STATE_TEMPLATE, "_current_operation", CONF_MODE_LIST + msg, CONF_MODE_STATE_TEMPLATE, "_attr_hvac_mode", CONF_MODE_LIST ) add_subscription(topics, CONF_MODE_STATE_TOPIC, handle_current_mode_received) @@ -583,7 +628,7 @@ def handle_fan_mode_received(msg): handle_mode_received( msg, CONF_FAN_MODE_STATE_TEMPLATE, - "_current_fan_mode", + "_attr_fan_mode", CONF_FAN_MODE_LIST, ) @@ -596,7 +641,7 @@ def handle_swing_mode_received(msg): handle_mode_received( msg, CONF_SWING_MODE_STATE_TEMPLATE, - "_current_swing_mode", + "_attr_swing_mode", CONF_SWING_MODE_LIST, ) @@ -629,7 +674,9 @@ def handle_onoff_mode_received(msg, template_name, attr): @log_messages(self.hass, self.entity_id) def handle_aux_mode_received(msg): """Handle receiving aux mode via MQTT.""" - handle_onoff_mode_received(msg, CONF_AUX_STATE_TEMPLATE, "_aux") + handle_onoff_mode_received( + msg, CONF_AUX_STATE_TEMPLATE, "_attr_is_aux_heat" + ) add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) @@ -639,13 +686,13 @@ def handle_preset_mode_received(msg): """Handle receiving preset mode via MQTT.""" preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: - self._preset_mode = None + self._attr_preset_mode = PRESET_NONE get_mqtt_data(self.hass).state_write_requests.write_state_request(self) return if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) return - if preset_mode not in self._preset_modes: + if preset_mode not in self.preset_modes: _LOGGER.warning( "'%s' received on topic %s. '%s' is not a valid preset mode", msg.payload, @@ -653,7 +700,8 @@ def handle_preset_mode_received(msg): preset_mode, ) else: - self._preset_mode = preset_mode + self._attr_preset_mode = preset_mode + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_subscription( @@ -668,85 +716,6 @@ async def _subscribe_topics(self): """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - if unit := self._config.get(CONF_TEMPERATURE_UNIT): - return unit - return self.hass.config.units.temperature_unit - - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self._current_temp - - @property - def target_temperature(self) -> float | None: - """Return the temperature we try to reach.""" - return self._target_temp - - @property - def target_temperature_low(self) -> float | None: - """Return the low target temperature we try to reach.""" - return self._target_temp_low - - @property - def target_temperature_high(self) -> float | None: - """Return the high target temperature we try to reach.""" - return self._target_temp_high - - @property - def hvac_action(self) -> HVACAction | None: - """Return the current running hvac operation if supported.""" - return self._action - - @property - def hvac_mode(self) -> HVACMode: - """Return current operation ie. heat, cool, idle.""" - return self._current_operation - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available operation modes.""" - return self._config[CONF_MODE_LIST] - - @property - def target_temperature_step(self) -> float: - """Return the supported step of target temperature.""" - return self._config[CONF_TEMP_STEP] - - @property - def preset_mode(self) -> str | None: - """Return preset mode.""" - if self._feature_preset_mode and self._preset_mode is not None: - return self._preset_mode - return PRESET_NONE - - @property - def preset_modes(self) -> list[str]: - """Return preset modes.""" - presets = [] - presets.extend(self._preset_modes) - if presets: - presets.insert(0, PRESET_NONE) - - return presets - - @property - def is_aux_heat(self) -> bool | None: - """Return true if away mode is on.""" - return self._aux - - @property - def fan_mode(self) -> str | None: - """Return the fan setting.""" - return self._current_fan_mode - - @property - def fan_modes(self) -> list[str]: - """Return the list of available fan modes.""" - return self._config[CONF_FAN_MODE_LIST] - async def _publish(self, topic, payload): if self._topic[topic] is not None: await self.async_publish( @@ -778,7 +747,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: CONF_TEMP_COMMAND_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, CONF_TEMP_STATE_TOPIC, - "_target_temp", + "_attr_target_temperature", ) await self._set_temperature( @@ -786,7 +755,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: CONF_TEMP_LOW_COMMAND_TOPIC, CONF_TEMP_LOW_COMMAND_TEMPLATE, CONF_TEMP_LOW_STATE_TOPIC, - "_target_temp_low", + "_attr_target_temperature_low", ) await self._set_temperature( @@ -794,7 +763,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: CONF_TEMP_HIGH_COMMAND_TOPIC, CONF_TEMP_HIGH_COMMAND_TEMPLATE, CONF_TEMP_HIGH_STATE_TOPIC, - "_target_temp_high", + "_attr_target_temperature_high", ) self.async_write_ha_state() @@ -805,7 +774,7 @@ async def async_set_swing_mode(self, swing_mode: str) -> None: await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: - self._current_swing_mode = swing_mode + self._attr_swing_mode = swing_mode self.async_write_ha_state() async def async_set_fan_mode(self, fan_mode: str) -> None: @@ -814,7 +783,7 @@ async def async_set_fan_mode(self, fan_mode: str) -> None: await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: - self._current_fan_mode = fan_mode + self._attr_fan_mode = fan_mode self.async_write_ha_state() async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -830,22 +799,12 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: await self._publish(CONF_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_MODE_STATE_TOPIC] is None: - self._current_operation = hvac_mode + self._attr_hvac_mode = hvac_mode self.async_write_ha_state() - @property - def swing_mode(self) -> str | None: - """Return the swing setting.""" - return self._current_swing_mode - - @property - def swing_modes(self) -> list[str]: - """List of available swing modes.""" - return self._config[CONF_SWING_MODE_LIST] - async def async_set_preset_mode(self, preset_mode: str) -> None: """Set a preset mode.""" - if self._feature_preset_mode: + if self._feature_preset_mode and self.preset_modes: if preset_mode not in self.preset_modes and preset_mode is not PRESET_NONE: _LOGGER.warning("'%s' is not a valid preset mode", preset_mode) return @@ -858,7 +817,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: ) if self._optimistic_preset_mode: - self._preset_mode = preset_mode if preset_mode != PRESET_NONE else None + self._attr_preset_mode = preset_mode self.async_write_ha_state() return @@ -870,7 +829,7 @@ async def _set_aux_heat(self, state): ) if self._topic[CONF_AUX_STATE_TOPIC] is None: - self._aux = state + self._attr_is_aux_heat = state self.async_write_ha_state() async def async_turn_aux_heat_on(self) -> None: @@ -880,60 +839,3 @@ async def async_turn_aux_heat_on(self) -> None: async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" await self._set_aux_heat(False) - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - support = 0 - - if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or ( - self._topic[CONF_TEMP_COMMAND_TOPIC] is not None - ): - support |= ClimateEntityFeature.TARGET_TEMPERATURE - - if (self._topic[CONF_TEMP_LOW_STATE_TOPIC] is not None) or ( - self._topic[CONF_TEMP_LOW_COMMAND_TOPIC] is not None - ): - support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - - if (self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is not None) or ( - self._topic[CONF_TEMP_HIGH_COMMAND_TOPIC] is not None - ): - support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - - if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or ( - self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None - ): - support |= ClimateEntityFeature.FAN_MODE - - if (self._topic[CONF_SWING_MODE_STATE_TOPIC] is not None) or ( - self._topic[CONF_SWING_MODE_COMMAND_TOPIC] is not None - ): - support |= ClimateEntityFeature.SWING_MODE - - if self._feature_preset_mode: - support |= ClimateEntityFeature.PRESET_MODE - - if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or ( - self._topic[CONF_AUX_COMMAND_TOPIC] is not None - ): - support |= ClimateEntityFeature.AUX_HEAT - - return support - - @property - def min_temp(self) -> float: - """Return the minimum temperature.""" - return self._config[CONF_TEMP_MIN] - - @property - def max_temp(self) -> float: - """Return the maximum temperature.""" - return self._config[CONF_TEMP_MAX] - - @property - def precision(self) -> float: - """Return the precision of the system.""" - if (precision := self._config.get(CONF_PRECISION)) is not None: - return precision - return super().precision From 08772004b360d83122021df54b9b6e3a47bff39b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Nov 2022 11:13:23 +0100 Subject: [PATCH 206/394] Fix SSDP failure to start on missing URLs (#81453) --- homeassistant/components/ssdp/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index d081ef877dee36..8e037602b90d78 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -52,7 +52,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.instance_id import async_get as async_get_instance_id -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_ssdp, bind_hass @@ -697,7 +697,16 @@ async def _async_start_upnp_servers(self) -> None: udn = await self._async_get_instance_udn() system_info = await async_get_system_info(self.hass) model_name = system_info["installation_type"] - presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False) + try: + presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False) + except NoURLAvailableError: + _LOGGER.warning( + "Could not set up UPnP/SSDP server, as a presentation URL could" + " not be determined; Please configure your internal URL" + " in the Home Assistant general configuration" + ) + return + serial_number = await async_get_instance_id(self.hass) HassUpnpServiceDevice.DEVICE_DEFINITION = ( HassUpnpServiceDevice.DEVICE_DEFINITION._replace( From 454f328a36f0b3a19058a50ad2b76c1a6dae73db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 11:14:01 +0100 Subject: [PATCH 207/394] Bump aiohomekit to 2.2.14 (#81454) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 18884d59307970..b2aec75c3ad9f7 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.13"], + "requirements": ["aiohomekit==2.2.14"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index e5973baf0cb17d..9e0c18e2988e02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,7 +171,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.13 +aiohomekit==2.2.14 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8dd8d3592fcf3c..5023fce634dc20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.13 +aiohomekit==2.2.14 # homeassistant.components.emulated_hue # homeassistant.components.http From 7556f2b84e892178cf8ba02c2ee91e0ddfb9e870 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Nov 2022 11:18:25 +0100 Subject: [PATCH 208/394] Update cryptography to 38.0.3 (#81455) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 626e88420ea574..17674a0b1b4bcf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ bluetooth-adapters==0.6.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 -cryptography==38.0.1 +cryptography==38.0.3 dbus-fast==1.61.1 fnvhash==0.1.0 hass-nabucasa==0.56.0 diff --git a/pyproject.toml b/pyproject.toml index f5b5908f0d27a7..3a74b2c5994c9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "lru-dict==1.1.8", "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==38.0.1", + "cryptography==38.0.3", "orjson==3.8.1", "pip>=21.0,<22.4", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index 962a9d59dc6d27..96a9f801df9a46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 -cryptography==38.0.1 +cryptography==38.0.3 orjson==3.8.1 pip>=21.0,<22.4 python-slugify==4.0.1 From 918940a0c681a453a4da46b8afbc5d6300eb1e4f Mon Sep 17 00:00:00 2001 From: hahn-th Date: Thu, 3 Nov 2022 12:03:49 +0100 Subject: [PATCH 209/394] Add HmIP-WGC to homematicip_cloud integration (#75733) * Add HmIP-WGC * remove unused code * removed test test_manually_configured_platform --- .../components/homematicip_cloud/button.py | 41 +++++++ .../components/homematicip_cloud/const.py | 1 + .../homematicip_cloud/test_button.py | 39 +++++++ .../homematicip_cloud/test_device.py | 2 +- tests/fixtures/homematicip_cloud.json | 105 ++++++++++++++++++ 5 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/homematicip_cloud/button.py create mode 100644 tests/components/homematicip_cloud/test_button.py diff --git a/homeassistant/components/homematicip_cloud/button.py b/homeassistant/components/homematicip_cloud/button.py new file mode 100644 index 00000000000000..3fb8ebe20bd201 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/button.py @@ -0,0 +1,41 @@ +"""Support for HomematicIP Cloud button devices.""" +from __future__ import annotations + +from homematicip.aio.device import AsyncWallMountedGarageDoorController + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN as HMIPC_DOMAIN, HomematicipGenericEntity +from .hap import HomematicipHAP + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the HomematicIP button from a config entry.""" + hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] + entities: list[HomematicipGenericEntity] = [] + for device in hap.home.devices: + if isinstance(device, AsyncWallMountedGarageDoorController): + entities.append(HomematicipGarageDoorControllerButton(hap, device)) + + if entities: + async_add_entities(entities) + + +class HomematicipGarageDoorControllerButton(HomematicipGenericEntity, ButtonEntity): + """Representation of the HomematicIP Wall mounted Garage Door Controller.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize a wall mounted garage door controller.""" + super().__init__(hap, device) + self._attr_icon = "mdi:arrow-up-down" + + async def async_press(self) -> None: + """Handle the button press.""" + await self._device.send_start_impulse() diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index a0f1c84015f7cc..055db90a68cb0d 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -10,6 +10,7 @@ PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.LIGHT, diff --git a/tests/components/homematicip_cloud/test_button.py b/tests/components/homematicip_cloud/test_button.py new file mode 100644 index 00000000000000..c399bc993275ae --- /dev/null +++ b/tests/components/homematicip_cloud/test_button.py @@ -0,0 +1,39 @@ +"""Tests for HomematicIP Cloud button.""" + +from unittest.mock import patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.button.const import SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.util import dt as dt_util + +from .helper import get_and_check_entity_basics + + +async def test_hmip_garage_door_controller_button(hass, default_mock_hap_factory): + """Test HomematicipGarageDoorControllerButton.""" + entity_id = "button.garagentor" + entity_name = "Garagentor" + device_model = "HmIP-WGC" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) + + get_and_check_entity_basics(hass, mock_hap, entity_id, entity_name, device_model) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.utcnow", return_value=now): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == now.isoformat() diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 44b91c4ed47930..52b92fdff9aa08 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 262 + assert len(mock_hap.hmip_device_by_entity_id) == 264 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index b0037aa380044c..5a7b05167637b9 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -6725,6 +6725,111 @@ "serializedGlobalTradeItemNumber": "3014F7110000000000STE2015", "type": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA", "updateState": "TRANSFERING_UPDATE" + }, + "3014F7110000000000000WGC": { + "availableFirmwareVersion": "1.0.2", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000WGC", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000011"], + "index": 0, + "label": "", + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -83, + "rssiPeerValue": -77, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": true, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000WGC", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F7110000000000000WGC", + "functionalChannelType": "IMPULSE_OUTPUT_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "impulseDuration": 0.4, + "index": 2, + "label": "Taste", + "processing": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000WGC", + "label": "Garagentor", + "lastStatusUpdate": 1630920800279, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 331, + "modelType": "HmIP-WGC", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000WGC", + "type": "WALL_MOUNTED_GARAGE_DOOR_CONTROLLER", + "updateState": "UP_TO_DATE" } }, "groups": { From 905005e1e80c61e3262c3f3e13af455b52fab59f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 12:51:38 +0100 Subject: [PATCH 210/394] Bump dbus-fast 1.64.0 (#81462) - Performance improvements changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.61.1...v1.64.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 89281323541cdb..bcb7ce9b15659c 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.61.1" + "dbus-fast==1.64.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 17674a0b1b4bcf..160f313d883623 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.3 -dbus-fast==1.61.1 +dbus-fast==1.64.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9e0c18e2988e02..5977ba9728686e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.61.1 +dbus-fast==1.64.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5023fce634dc20..a90f9060d0661a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,7 +423,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.61.1 +dbus-fast==1.64.0 # homeassistant.components.debugpy debugpy==1.6.3 From dcd1ab7ec3ec0518493045d2e66db2cb258e589c Mon Sep 17 00:00:00 2001 From: Dennis Schroer Date: Thu, 3 Nov 2022 12:53:58 +0100 Subject: [PATCH 211/394] Update energyflip-client dependency to 0.2.2 (#81426) --- homeassistant/components/huisbaasje/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json index 2963a82512bda0..47d47da182b552 100644 --- a/homeassistant/components/huisbaasje/manifest.json +++ b/homeassistant/components/huisbaasje/manifest.json @@ -3,7 +3,7 @@ "name": "Huisbaasje", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huisbaasje", - "requirements": ["energyflip-client==0.2.1"], + "requirements": ["energyflip-client==0.2.2"], "codeowners": ["@dennisschroer"], "iot_class": "cloud_polling", "loggers": ["huisbaasje"] diff --git a/requirements_all.txt b/requirements_all.txt index 5977ba9728686e..6bb67baf2ef035 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -626,7 +626,7 @@ elmax_api==0.0.2 emulated_roku==0.2.1 # homeassistant.components.huisbaasje -energyflip-client==0.2.1 +energyflip-client==0.2.2 # homeassistant.components.enocean enocean==0.50 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a90f9060d0661a..e4fc9f18e79292 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -479,7 +479,7 @@ elmax_api==0.0.2 emulated_roku==0.2.1 # homeassistant.components.huisbaasje -energyflip-client==0.2.1 +energyflip-client==0.2.2 # homeassistant.components.enocean enocean==0.50 From b3403d7fca602603fe92c862b852ad25fcc64788 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 3 Nov 2022 13:06:53 +0100 Subject: [PATCH 212/394] Improve MQTT type hints part 3 (#80542) * Improve typing debug_info * Improve typing device_automation * Improve typing device_trigger * Improve typing fan * Additional type hints device_trigger * Set fan type hints to class level * Cleanup and mypy * Follow up and missed hint * Follow up comment --- homeassistant/components/mqtt/debug_info.py | 2 +- .../components/mqtt/device_automation.py | 16 ++- .../components/mqtt/device_trigger.py | 21 ++-- homeassistant/components/mqtt/fan.py | 109 ++++++++++-------- 4 files changed, 90 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index 5fae98eaea52cb..bdbdd74de96797 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -28,7 +28,7 @@ def log_messages( debug_info_entities = get_mqtt_data(hass).debug_info_entities - def _log_message(msg): + def _log_message(msg: Any) -> None: """Log message.""" messages = debug_info_entities[entity_id]["subscriptions"][ msg.subscribed_topic diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 0646a5bda0c908..a1bc2cdeb3ea3c 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -1,9 +1,14 @@ """Provides device automations for MQTT.""" +from __future__ import annotations + import functools import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import device_trigger from .config import MQTT_BASE_SCHEMA @@ -20,14 +25,19 @@ ).extend(MQTT_BASE_SCHEMA.schema) -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Set up MQTT device automation dynamically through MQTT discovery.""" setup = functools.partial(_async_setup_automation, hass, config_entry=config_entry) await async_setup_entry_helper(hass, "device_automation", setup, PLATFORM_SCHEMA) -async def _async_setup_automation(hass, config, config_entry, discovery_data): +async def _async_setup_automation( + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType, +) -> None: """Set up an MQTT device automation.""" if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER: await device_trigger.async_setup_trigger( @@ -35,6 +45,6 @@ async def _async_setup_automation(hass, config, config_entry, discovery_data): ) -async def async_removed_from_device(hass, device_id): +async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None: """Handle Mqtt removed from a device.""" await device_trigger.async_removed_from_device(hass, device_id) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index f51731284ccb82..e8bcf1abc48900 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -3,7 +3,7 @@ from collections.abc import Callable import logging -from typing import cast +from typing import Any, cast import attr import voluptuous as vol @@ -23,7 +23,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, trigger as mqtt_trigger from .config import MQTT_BASE_SCHEMA @@ -35,7 +35,7 @@ CONF_TOPIC, DOMAIN, ) -from .discovery import MQTT_DISCOVERY_DONE +from .discovery import MQTT_DISCOVERY_DONE, MQTTDiscoveryPayload from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, MqttDiscoveryDeviceUpdate, @@ -96,7 +96,7 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" - mqtt_config = { + mqtt_config: dict[str, Any] = { CONF_PLATFORM: DOMAIN, CONF_TOPIC: self.trigger.topic, CONF_ENCODING: DEFAULT_ENCODING, @@ -123,7 +123,7 @@ class Trigger: """Device trigger settings.""" device_id: str = attr.ib() - discovery_data: dict | None = attr.ib() + discovery_data: DiscoveryInfoType | None = attr.ib() hass: HomeAssistant = attr.ib() payload: str | None = attr.ib() qos: int | None = attr.ib() @@ -193,7 +193,7 @@ def __init__( hass: HomeAssistant, config: ConfigType, device_id: str, - discovery_data: dict, + discovery_data: DiscoveryInfoType, config_entry: ConfigEntry, ) -> None: """Initialize.""" @@ -237,7 +237,7 @@ async def async_setup(self) -> None: self.hass, discovery_hash, self.discovery_data, self.device_id ) - async def async_update(self, discovery_data: dict) -> None: + async def async_update(self, discovery_data: MQTTDiscoveryPayload) -> None: """Handle MQTT device trigger discovery updates.""" discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH] discovery_id = discovery_hash[1] @@ -261,11 +261,14 @@ async def async_tear_down(self) -> None: async def async_setup_trigger( - hass, config: ConfigType, config_entry: ConfigEntry, discovery_data: dict + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType, ) -> None: """Set up the MQTT device trigger.""" config = TRIGGER_DISCOVERY_SCHEMA(config) - discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH] if (device_id := update_device(hass, config_entry, config)) is None: async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 1b6c3e425a48e2..8a65b909eb8804 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,6 +1,7 @@ """Support for MQTT fans.""" from __future__ import annotations +from collections.abc import Callable import functools import logging import math @@ -27,6 +28,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.percentage import ( int_states_in_range, @@ -54,7 +56,13 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" @@ -110,18 +118,18 @@ _LOGGER = logging.getLogger(__name__) -def valid_speed_range_configuration(config): +def valid_speed_range_configuration(config: ConfigType) -> ConfigType: """Validate that the fan speed_range configuration is valid, throws if it isn't.""" - if config.get(CONF_SPEED_RANGE_MIN) == 0: + if config[CONF_SPEED_RANGE_MIN] == 0: raise ValueError("speed_range_min must be > 0") - if config.get(CONF_SPEED_RANGE_MIN) >= config.get(CONF_SPEED_RANGE_MAX): + if config[CONF_SPEED_RANGE_MIN] >= config[CONF_SPEED_RANGE_MAX]: raise ValueError("speed_range_max must be > speed_range_min") return config -def valid_preset_mode_configuration(config): +def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the preset mode reset payload is not one of the preset modes.""" - if config.get(CONF_PAYLOAD_RESET_PRESET_MODE) in config.get(CONF_PRESET_MODES_LIST): + if config[CONF_PAYLOAD_RESET_PRESET_MODE] in config[CONF_PRESET_MODES_LIST]: raise ValueError("preset_modes must not contain payload_reset_preset_mode") return config @@ -250,8 +258,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT fan.""" async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) @@ -263,32 +271,41 @@ class MqttFan(MqttEntity, FanEntity): _entity_id_format = fan.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] + _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _feature_percentage: bool + _feature_preset_mode: bool + _topic: dict[str, Any] + _optimistic: bool + _optimistic_oscillation: bool + _optimistic_percentage: bool + _optimistic_preset_mode: bool + _payload: dict[str, Any] + _speed_range: tuple[int, int] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT fan.""" self._attr_percentage = None self._attr_preset_mode = None - self._topic = None - self._payload = None - self._value_templates = None - self._command_templates = None - self._optimistic = None - self._optimistic_oscillation = None - self._optimistic_percentage = None - self._optimistic_preset_mode = None - MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._speed_range = ( - config.get(CONF_SPEED_RANGE_MIN), - config.get(CONF_SPEED_RANGE_MAX), + config[CONF_SPEED_RANGE_MIN], + config[CONF_SPEED_RANGE_MAX], ) self._topic = { key: config.get(key) @@ -303,18 +320,6 @@ def _setup_from_config(self, config): CONF_OSCILLATION_COMMAND_TOPIC, ) } - self._value_templates = { - CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), - ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE), - ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE), - ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE), - } - self._command_templates = { - CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), - ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE), - ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE), - ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE), - } self._payload = { "STATE_ON": config[CONF_PAYLOAD_ON], "STATE_OFF": config[CONF_PAYLOAD_OFF], @@ -359,24 +364,38 @@ def _setup_from_config(self, config): if self._feature_preset_mode: self._attr_supported_features |= FanEntityFeature.PRESET_MODE - for key, tpl in self._command_templates.items(): + command_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), + ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_COMMAND_TEMPLATE), + ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_COMMAND_TEMPLATE), + ATTR_OSCILLATING: config.get(CONF_OSCILLATION_COMMAND_TEMPLATE), + } + self._command_templates = {} + for key, tpl in command_templates.items(): self._command_templates[key] = MqttCommandTemplate( tpl, entity=self ).async_render - for key, tpl in self._value_templates.items(): + self._value_templates = {} + value_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + ATTR_PERCENTAGE: config.get(CONF_PERCENTAGE_VALUE_TEMPLATE), + ATTR_PRESET_MODE: config.get(CONF_PRESET_MODE_VALUE_TEMPLATE), + ATTR_OSCILLATING: config.get(CONF_OSCILLATION_VALUE_TEMPLATE), + } + for key, tpl in value_templates.items(): self._value_templates[key] = MqttValueTemplate( tpl, entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, Any] = {} @callback @log_messages(self.hass, self.entity_id) - def state_received(msg): + def state_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message.""" payload = self._value_templates[CONF_STATE](msg.payload) if not payload: @@ -400,7 +419,7 @@ def state_received(msg): @callback @log_messages(self.hass, self.entity_id) - def percentage_received(msg): + def percentage_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for the percentage.""" rendered_percentage_payload = self._value_templates[ATTR_PERCENTAGE]( msg.payload @@ -446,9 +465,9 @@ def percentage_received(msg): @callback @log_messages(self.hass, self.entity_id) - def preset_mode_received(msg): + def preset_mode_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for preset mode.""" - preset_mode = self._value_templates[ATTR_PRESET_MODE](msg.payload) + preset_mode = str(self._value_templates[ATTR_PRESET_MODE](msg.payload)) if preset_mode == self._payload["PRESET_MODE_RESET"]: self._attr_preset_mode = None self.async_write_ha_state() @@ -456,7 +475,7 @@ def preset_mode_received(msg): if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) return - if preset_mode not in self.preset_modes: + if not self.preset_modes or preset_mode not in self.preset_modes: _LOGGER.warning( "'%s' received on topic %s. '%s' is not a valid preset mode", msg.payload, @@ -479,7 +498,7 @@ def preset_mode_received(msg): @callback @log_messages(self.hass, self.entity_id) - def oscillation_received(msg): + def oscillation_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for the oscillation.""" payload = self._value_templates[ATTR_OSCILLATING](msg.payload) if not payload: @@ -504,7 +523,7 @@ def oscillation_received(msg): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) From cd5901e0d03a80d1911bc5a7bca87736bef1a990 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 14:49:12 +0100 Subject: [PATCH 213/394] Fix HomeKit thermostat to take priority over fans (#81473) --- homeassistant/components/homekit/type_thermostats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a924548816b745..a8c7a53718ae0b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -306,7 +306,7 @@ def __init__(self, *args): if attributes.get(ATTR_HVAC_ACTION) is not None: self.fan_chars.append(CHAR_CURRENT_FAN_STATE) serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars) - serv_thermostat.add_linked_service(serv_fan) + serv_fan.add_linked_service(serv_thermostat) self.char_active = serv_fan.configure_char( CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active ) From 9c85d22bab333556efd4194fc74842b044412cfe Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 3 Nov 2022 10:39:02 -0400 Subject: [PATCH 214/394] Bump AIOAladdinConnect to 0.1.47 (#81479) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 50ab6af6f851c4..6888eb4d8b270c 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.46"], + "requirements": ["AIOAladdinConnect==0.1.47"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 6bb67baf2ef035..9a43c575b9a188 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.46 +AIOAladdinConnect==0.1.47 # homeassistant.components.adax Adax-local==0.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4fc9f18e79292..bf7be0f33a6275 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.46 +AIOAladdinConnect==0.1.47 # homeassistant.components.adax Adax-local==0.1.5 From 08936e40414843f8140f305d49d01885ec968f45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Nov 2022 20:35:05 +0100 Subject: [PATCH 215/394] Bump oralb-ble to 0.10.1 (#81491) fixes #81489 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.0...v0.10.1 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index cad6167228cce4..520306aed037d2 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.0"], + "requirements": ["oralb-ble==0.10.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 9a43c575b9a188..13087d43f40d29 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1241,7 +1241,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.0 +oralb-ble==0.10.1 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf7be0f33a6275..98ee447b345d60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.0 +oralb-ble==0.10.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From dd5baa6e488d8f104482fdd4f70bfe0fac13a4b2 Mon Sep 17 00:00:00 2001 From: Renat Sibgatulin Date: Thu, 3 Nov 2022 22:13:57 +0000 Subject: [PATCH 216/394] Add air-Q integration (air quality sensors) (#76999) * Added initial files for air-Q integration * Allow FIXME comments in pylint (temporary) Also reintroduce --ignore-missing-annotations=y * Set up air-q entry to connect to aioairq's API (initial attempt) Also add necessary constants * Implement a class for sensors and its update logic Very early stage, WIP * Zeroconf and authentication are working * Complete the bare-bone minimal working version Specifically, make AirQSensor update its values. * Handle invalid authentication gracefully * Handle ClientConnectionError gracefully * Add field hint for the login form The key in the schema, which defines the form in `ConfigFlow.async_show_form` is looked up in both `airq/strings/json` and `airq/translations/en.json`. I am still not 100% sure how this lookup is performed. WIP * Minor cleanups * Extend sensor list to all supported by SensorDeviceClass Also manage warming up sensors * aioairq is published to PyPI and mentioned in requirements * Reordered constants and list content alphabetically As required by style guides. Also turned SENSOR_TYPES to a list * Updated file docstrings for a dev unfamiliar w/homeassistant like myself * Adding a bit of logging for the integration setup process * Expose scan interval & smoothing flag Also streamline test_authentication in config_flow. * Fix a type annotation mistake * Use as many constants from homeassistant.const as possible My only concern is using CONST_IP_ADDRESS = "ip_address" for smth which stands for both IP address and mDNS... * Temporarily rollback ConfigFlow.async_step_configure and use defaults TODO: implement OptionFlowHandler instead * Define custom Coordinator, w subset of airq config The latter is then accessed from entity / sensor constructors to define correct DeviceInfo * Provide translations to de & fr + minor changes to en * Provide translations to ru + a minor en changes * Make translation a little more helpful and polite * Fix devicename and entry title * Remove stale commented out code * Test config_flow At this point two helper functions which interact with the external library are not tested * Clean up unrelated and meant as temporary changes * Clean up unnecessary comments meant for internal use * Move fetching config to a dedicated async coordinator method As opposed to it being a potentially poorly justified step in async_setup_entry * Remove zeroconf support since it is not yet ready * Remove translations other than en * Remove unnecessary comments, manifest.json entries, and constants * Improve exception handling - `_LOGGER` uses `debug` and not `error` levels. - Drop `ClientConnect` and catch `aiohttop.ClientConnectError` directly - Drop `Exception` as it is not expected from `aioairq` (remove the corresponding test too) * Drop strings for obsolete errors and steps Specifically, `unknown` error isn't caught any more. `configure` step has also been removed. * Refactor en.json to be consistent with strings.json * Move target_route from a coordinator argument to a constant At this point a user cannot configure the target_route route, thus it does not make sense to expose it half-heartedly in `AirQCoordinator.__init__`, since it cannot be accessed. * Fix an async call in `AirQCoordinator.async_setup_entry` * Refactor underlying aioairq API - Use `homeassistant.helpers.aiohttp.async_get_clientsession` and pass a single persistent session to `aioariq.AirQ.__init__` - `aioairq.AirQ.fetch_device_info` now returns a `DeviceInfo` object heavily inspired and almost compatible with `homeassistant.helpers.entity.DeviceInfo`. Make heavier use of this object and define a single `DeviceInfo` in the `AirQCoordinator` (instead of recreating the same object for each sensor of the device in `sensor.AirQSensor`) - Drop two helper functions in `config_flow.py` and operate on `aioariq.AirQ` methods directly * Fix the version of aioairq * Add 15 more sensors + icons * Remove cnt* & TypPS, change units of health & performance * Add 12 more sensors * Add a missing icon * Gracefully handle device not being available on setup If the device and the host are not on the same WiFi, ServerTimeoutError is raised, which is caught by ClientConnectionError. If the device is powered off, ClientConnectionError is expected. In both cases, ConfigEntryNotReady is raised, as prescribed by the docs. Newer version of aioairq times-out far quicker than the default 5 mins. * Rename two sensors * Validate provided IP address / mDNS aioairq now raises InvalidInput if provided IP / mDNS does not seem valid. Handle this exception correctly * Apply suggestions from code review Clean up the comments and rename the logger Co-authored-by: Erik Montnemery Co-authored-by: Artem Draft * Only fetch device info during the first refresh - Fetched info is stored in AirQCoordinator.device_info. - In `AirQSensor.native_value` only multiply by the factor if the sensor reading is not None - Fix the tests for ConfigFlow for aioairq==0.2.3. Specifically make the dummy data pass the new validation step upstream + add a test which fails it * Drop custom device classes for now * Apply suggestions from code review Co-authored-by: Artem Draft * Only fetch device info during ConfigFlow.async_step_user Store the result obtained by `airq.fetch_device_info` it in `config_entry.data`. Pass the entire config entry to `AirQCoordinator` and build the entire `homeassistant.helpers.entity.DeviceInfo` in the `AirQCoordinator.__init__`. This makes `AirQCoordinator._async_fetch_device_info` and overloaded `AirQCoordinator._async_config_entry_first_refresh` obsolete. Bump aioairq version. Turn update_interval from `AirQCoordinator.__init__` argument into a contestant. * Custom entity description exposing a hook to modify sensor value Use a `AirQEntityDescription` with a callable `value_fn` which allows to change the sensor value retrieved from the device. Note that the callable does not handle data retrieval itself (even from `coordinator.data`). Instead it is purely a hook to transform obtained value. * Avoid duplicated use of unique_id Device info is fetched during the `ConfigFlow.async_user_step`. `unique_id` is taken from the device info and is **not** stored in `config_entry.data`. Subsequently `config_entry.unique_id` is used instead. * Drop unnecessary try-except Co-authored-by: Artem Draft * Clarify the use of value_transform_fn * Refactor the use of lambdas in AirQEntityDescription Now it is the job of the callable under `value` to get the sensor reading from the coordinator's data. Factoring this functionality into a callback decouples the key of the description from the key of dict, returned by the API, so `AirQEntityDescription` no longer requires its key to be set to smth clearly internal (e.g. `nh3_MR100`). * Use a callback to update native_value Since all `native_value`s are updated synchronously, it can as well be done in a callback for better state consistency (right?) * Revert the description keys to match data keys Must match given the current way of identifying available sensors. On a broader scale, they must match to be able to relate the descriptions to sensors, unless a separate lookup table is maintained. * Reduce number of loops when adding sensors Filtering warming up sensors and non-sensor keys can be combined with adding entities. * Remove obsolete imports * Update integrations.json * Add integration_type Integration supports multiple devices => hub Co-authored-by: dl2080 Co-authored-by: Erik Montnemery Co-authored-by: Artem Draft Co-authored-by: Daniel Lehmann <43613560+dl2080@users.noreply.github.com> Co-authored-by: Martin Selbmann --- .coveragerc | 2 + CODEOWNERS | 2 + homeassistant/components/airq/__init__.py | 78 ++++ homeassistant/components/airq/config_flow.py | 84 ++++ homeassistant/components/airq/const.py | 9 + homeassistant/components/airq/manifest.json | 11 + homeassistant/components/airq/sensor.py | 361 ++++++++++++++++++ homeassistant/components/airq/strings.json | 22 ++ .../components/airq/translations/en.json | 23 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/airq/__init__.py | 1 + tests/components/airq/test_config_flow.py | 93 +++++ 15 files changed, 699 insertions(+) create mode 100644 homeassistant/components/airq/__init__.py create mode 100644 homeassistant/components/airq/config_flow.py create mode 100644 homeassistant/components/airq/const.py create mode 100644 homeassistant/components/airq/manifest.json create mode 100644 homeassistant/components/airq/sensor.py create mode 100644 homeassistant/components/airq/strings.json create mode 100644 homeassistant/components/airq/translations/en.json create mode 100644 tests/components/airq/__init__.py create mode 100644 tests/components/airq/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 78168feced05c4..eb60f320a749dd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -35,6 +35,8 @@ omit = homeassistant/components/agent_dvr/helpers.py homeassistant/components/airnow/__init__.py homeassistant/components/airnow/sensor.py + homeassistant/components/airq/__init__.py + homeassistant/components/airq/sensor.py homeassistant/components/airthings/__init__.py homeassistant/components/airthings/sensor.py homeassistant/components/airthings_ble/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index cb4e68f1535c74..cbff4badc9fa54 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -45,6 +45,8 @@ build.json @home-assistant/supervisor /tests/components/airly/ @bieniu /homeassistant/components/airnow/ @asymworks /tests/components/airnow/ @asymworks +/homeassistant/components/airq/ @Sibgatulin @dl2080 +/tests/components/airq/ @Sibgatulin @dl2080 /homeassistant/components/airthings/ @danielhiversen /tests/components/airthings/ @danielhiversen /homeassistant/components/airthings_ble/ @vincegio diff --git a/homeassistant/components/airq/__init__.py b/homeassistant/components/airq/__init__.py new file mode 100644 index 00000000000000..4bc64e1e825129 --- /dev/null +++ b/homeassistant/components/airq/__init__.py @@ -0,0 +1,78 @@ +"""The air-Q integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioairq import AirQ + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +class AirQCoordinator(DataUpdateCoordinator): + """Coordinator is responsible for querying the device at a specified route.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + ) -> None: + """Initialise a custom coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + session = async_get_clientsession(hass) + self.airq = AirQ( + entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], session + ) + self.device_id = entry.unique_id + assert self.device_id is not None + self.device_info = DeviceInfo( + manufacturer=MANUFACTURER, + identifiers={(DOMAIN, self.device_id)}, + ) + self.device_info.update(entry.data["device_info"]) + + async def _async_update_data(self) -> dict: + """Fetch the data from the device.""" + data = await self.airq.get(TARGET_ROUTE) + return self.airq.drop_uncertainties_from_data(data) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up air-Q from a config entry.""" + + coordinator = AirQCoordinator(hass, entry) + + # Query the device for the first time and initialise coordinator.data + await coordinator.async_config_entry_first_refresh() + + # Record the coordinator in a global store + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + 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): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/airq/config_flow.py b/homeassistant/components/airq/config_flow.py new file mode 100644 index 00000000000000..05af6825233cc6 --- /dev/null +++ b/homeassistant/components/airq/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for air-Q integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aioairq import AirQ, InvalidAuth, InvalidInput +from aiohttp.client_exceptions import ClientConnectionError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_IP_ADDRESS): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for air-Q.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial (authentication) configuration step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors: dict[str, str] = {} + + session = async_get_clientsession(self.hass) + try: + airq = AirQ(user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], session) + except InvalidInput: + _LOGGER.debug( + "%s does not appear to be a valid IP address or mDNS name", + user_input[CONF_IP_ADDRESS], + ) + errors["base"] = "invalid_input" + else: + try: + await airq.validate() + except ClientConnectionError: + _LOGGER.debug( + "Failed to connect to device %s. Check the IP address / device ID " + "as well as whether the device is connected to power and the WiFi", + user_input[CONF_IP_ADDRESS], + ) + errors["base"] = "cannot_connect" + except InvalidAuth: + _LOGGER.debug( + "Incorrect password for device %s", user_input[CONF_IP_ADDRESS] + ) + errors["base"] = "invalid_auth" + else: + _LOGGER.debug( + "Successfully connected to %s", user_input[CONF_IP_ADDRESS] + ) + + device_info = await airq.fetch_device_info() + await self.async_set_unique_id(device_info.pop("id")) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=device_info["name"], + data=user_input | {"device_info": device_info}, + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/airq/const.py b/homeassistant/components/airq/const.py new file mode 100644 index 00000000000000..82719515cbfb02 --- /dev/null +++ b/homeassistant/components/airq/const.py @@ -0,0 +1,9 @@ +"""Constants for the air-Q integration.""" +from typing import Final + +DOMAIN: Final = "airq" +MANUFACTURER: Final = "CorantGmbH" +TARGET_ROUTE: Final = "average" +CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³" +ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³" +UPDATE_INTERVAL: float = 10.0 diff --git a/homeassistant/components/airq/manifest.json b/homeassistant/components/airq/manifest.json new file mode 100644 index 00000000000000..932b404278d054 --- /dev/null +++ b/homeassistant/components/airq/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "airq", + "name": "air-Q", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airq", + "requirements": ["aioairq==0.2.4"], + "codeowners": ["@Sibgatulin", "@dl2080"], + "iot_class": "local_polling", + "loggers": ["aioairq"], + "integration_type": "hub" +} diff --git a/homeassistant/components/airq/sensor.py b/homeassistant/components/airq/sensor.py new file mode 100644 index 00000000000000..c524050ea663c7 --- /dev/null +++ b/homeassistant/components/airq/sensor.py @@ -0,0 +1,361 @@ +"""Definition of air-Q sensor platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +import logging +from typing import Literal + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + PRESSURE_HPA, + SOUND_PRESSURE_WEIGHTED_DBA, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import AirQCoordinator +from .const import ( + ACTIVITY_BECQUEREL_PER_CUBIC_METER, + CONCENTRATION_GRAMS_PER_CUBIC_METER, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class AirQEntityDescriptionMixin: + """Class for keys required by AirQ entity.""" + + value: Callable[[dict], float | int | None] + + +@dataclass +class AirQEntityDescription(SensorEntityDescription, AirQEntityDescriptionMixin): + """Describes AirQ sensor entity.""" + + +# Keys must match those in the data dictionary +SENSOR_TYPES: list[AirQEntityDescription] = [ + AirQEntityDescription( + key="nh3_MR100", + name="Ammonia", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("nh3_MR100"), + ), + AirQEntityDescription( + key="cl2_M20", + name="Chlorine", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("cl2_M20"), + ), + AirQEntityDescription( + key="co", + name="CO", + device_class=SensorDeviceClass.CO, + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("co"), + ), + AirQEntityDescription( + key="co2", + name="CO2", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("co2"), + ), + AirQEntityDescription( + key="dewpt", + name="Dew point", + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("dewpt"), + icon="mdi:water-thermometer", + ), + AirQEntityDescription( + key="ethanol", + name="Ethanol", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("ethanol"), + ), + AirQEntityDescription( + key="ch2o_M10", + name="Formaldehyde", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("ch2o_M10"), + ), + AirQEntityDescription( + key="h2s", + name="H2S", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("h2s"), + ), + AirQEntityDescription( + key="health", + name="Health Index", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:heart-pulse", + value=lambda data: data.get("health", 0.0) / 10.0, + ), + AirQEntityDescription( + key="humidity", + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("humidity"), + ), + AirQEntityDescription( + key="humidity_abs", + name="Absolute humidity", + native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("humidity_abs"), + icon="mdi:water", + ), + AirQEntityDescription( + key="h2_M1000", + name="Hydrogen", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("h2_M1000"), + ), + AirQEntityDescription( + key="ch4_MIPEX", + name="Methane", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("ch4_MIPEX"), + ), + AirQEntityDescription( + key="n2o", + name="N2O", + device_class=SensorDeviceClass.NITROUS_OXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("n2o"), + ), + AirQEntityDescription( + key="no_M250", + name="NO", + device_class=SensorDeviceClass.NITROGEN_MONOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("no_M250"), + ), + AirQEntityDescription( + key="no2", + name="NO2", + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("no2"), + ), + AirQEntityDescription( + key="o3", + name="Ozone", + device_class=SensorDeviceClass.OZONE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("o3"), + ), + AirQEntityDescription( + key="oxygen", + name="Oxygen", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("oxygen"), + icon="mdi:leaf", + ), + AirQEntityDescription( + key="performance", + name="Performance Index", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:head-check", + value=lambda data: data.get("performance", 0.0) / 10.0, + ), + AirQEntityDescription( + key="pm1", + name="PM1", + device_class=SensorDeviceClass.PM1, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pm1"), + icon="mdi:dots-hexagon", + ), + AirQEntityDescription( + key="pm2_5", + name="PM2.5", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pm2_5"), + icon="mdi:dots-hexagon", + ), + AirQEntityDescription( + key="pm10", + name="PM10", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pm10"), + icon="mdi:dots-hexagon", + ), + AirQEntityDescription( + key="pressure", + name="Pressure", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pressure"), + ), + AirQEntityDescription( + key="pressure_rel", + name="Relative pressure", + native_unit_of_measurement=PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("pressure_rel"), + icon="mdi:gauge", + ), + AirQEntityDescription( + key="c3h8_MIPEX", + name="Propane", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("c3h8_MIPEX"), + ), + AirQEntityDescription( + key="so2", + name="SO2", + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("so2"), + ), + AirQEntityDescription( + key="sound", + name="Noise", + native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("sound"), + icon="mdi:ear-hearing", + ), + AirQEntityDescription( + key="sound_max", + name="Noise (Maximum)", + native_unit_of_measurement=SOUND_PRESSURE_WEIGHTED_DBA, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("sound_max"), + icon="mdi:ear-hearing", + ), + AirQEntityDescription( + key="radon", + name="Radon", + native_unit_of_measurement=ACTIVITY_BECQUEREL_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("radon"), + icon="mdi:radioactive", + ), + AirQEntityDescription( + key="temperature", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("temperature"), + ), + AirQEntityDescription( + key="tvoc", + name="VOC", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("tvoc"), + ), + AirQEntityDescription( + key="tvoc_ionsc", + name="VOC (Industrial)", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.get("tvoc_ionsc"), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensor entities based on a config entry.""" + + coordinator = hass.data[DOMAIN][config.entry_id] + + entities: list[AirQSensor] = [] + + device_status: dict[str, str] | Literal["OK"] = coordinator.data["Status"] + + for description in SENSOR_TYPES: + if description.key not in coordinator.data: + if isinstance( + device_status, dict + ) and "sensor still in warm up phase" in device_status.get( + description.key, "OK" + ): + # warming up sensors do not contribute keys to coordinator.data + # but still must be added + _LOGGER.debug("Following sensor is warming up: %s", description.key) + else: + continue + entities.append(AirQSensor(coordinator, description)) + + async_add_entities(entities) + + +class AirQSensor(CoordinatorEntity, SensorEntity): + """Representation of a Sensor.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AirQCoordinator, + description: AirQEntityDescription, + ) -> None: + """Initialize a single sensor.""" + super().__init__(coordinator) + self.entity_description: AirQEntityDescription = description + + self._attr_device_info = coordinator.device_info + self._attr_name = description.name + self._attr_unique_id = f"{coordinator.device_id}_{description.key}" + self._attr_native_value = description.value(coordinator.data) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = self.entity_description.value(self.coordinator.data) + self.async_write_ha_state() diff --git a/homeassistant/components/airq/strings.json b/homeassistant/components/airq/strings.json new file mode 100644 index 00000000000000..3618d9d517e092 --- /dev/null +++ b/homeassistant/components/airq/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "title": "Identify the device", + "description": "Provide the IP address or mDNS of the device and its password", + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]", + "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_input": "[%key:common::config_flow::error::invalid_host%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/airq/translations/en.json b/homeassistant/components/airq/translations/en.json new file mode 100644 index 00000000000000..81b8c1ff83e622 --- /dev/null +++ b/homeassistant/components/airq/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "title": "Identify the device", + "description": "Provide the IP address or mDNS of the device and its password", + "data": { + "ip_address": "Device IP or mDNS (e.g. '123ab_air-q.local')", + "password": "Password" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please check the IP or mDNS", + "invalid_auth": "Wrong password", + "invalid_input": "Invalid IP address or mDNS " + }, + "abort": { + "already_configured": "This device is already configured and available to Home Assistant" + } + } +} + diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5214436be42ede..b0cd687b54e13b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -15,6 +15,7 @@ "agent_dvr", "airly", "airnow", + "airq", "airthings", "airthings_ble", "airtouch4", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 1d04bde132ab7e..4d1b9da50af12c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -89,6 +89,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "airq": { + "name": "air-Q", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "airthings": { "name": "Airthings", "integrations": { diff --git a/requirements_all.txt b/requirements_all.txt index 13087d43f40d29..225d00966846d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,6 +112,9 @@ aio_geojson_usgs_earthquakes==0.1 # homeassistant.components.gdacs aio_georss_gdacs==0.7 +# homeassistant.components.airq +aioairq==0.2.4 + # homeassistant.components.airzone aioairzone==0.4.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98ee447b345d60..5a7f2351050f03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -99,6 +99,9 @@ aio_geojson_usgs_earthquakes==0.1 # homeassistant.components.gdacs aio_georss_gdacs==0.7 +# homeassistant.components.airq +aioairq==0.2.4 + # homeassistant.components.airzone aioairzone==0.4.8 diff --git a/tests/components/airq/__init__.py b/tests/components/airq/__init__.py new file mode 100644 index 00000000000000..612761c0653081 --- /dev/null +++ b/tests/components/airq/__init__.py @@ -0,0 +1 @@ +"""Tests for the air-Q integration.""" diff --git a/tests/components/airq/test_config_flow.py b/tests/components/airq/test_config_flow.py new file mode 100644 index 00000000000000..38fc15fdae3cf8 --- /dev/null +++ b/tests/components/airq/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the air-Q config flow.""" +from unittest.mock import patch + +from aioairq.core import DeviceInfo, InvalidAuth, InvalidInput +from aiohttp.client_exceptions import ClientConnectionError + +from homeassistant import config_entries +from homeassistant.components.airq.const import DOMAIN +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +TEST_USER_DATA = { + CONF_IP_ADDRESS: "192.168.0.0", + CONF_PASSWORD: "password", +} +TEST_DEVICE_INFO = DeviceInfo( + id="id", + name="name", + model="model", + sw_version="sw", + hw_version="hw", +) +TEST_DATA_OUT = TEST_USER_DATA | { + "device_info": {k: v for k, v in TEST_DEVICE_INFO.items() if k != "id"} +} + + +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("aioairq.AirQ.validate"), patch( + "aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + TEST_USER_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == TEST_DEVICE_INFO["name"] + assert result2["data"] == TEST_DATA_OUT + + +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("aioairq.AirQ.validate", side_effect=InvalidAuth): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA | {CONF_PASSWORD: "wrong_password"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioairq.AirQ.validate", side_effect=ClientConnectionError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_input(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aioairq.AirQ.validate", side_effect=InvalidInput): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_USER_DATA | {CONF_IP_ADDRESS: "invalid_ip"} + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_input"} From 71c18ec5271e6501667deb8d929df50954553dea Mon Sep 17 00:00:00 2001 From: krazos Date: Thu, 3 Nov 2022 18:23:42 -0400 Subject: [PATCH 217/394] Fix errant reference to "Solar.Forecast" in "Forecast.Solar" config options (#81252) Fix errant reference to Solar.Forecast Config options string referred to "Solar.Forecast". That reference has been corrected to "Forecast.Solar". --- homeassistant/components/forecast_solar/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index b10e927eb8b528..041935c59efb37 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -17,7 +17,7 @@ "options": { "step": { "init": { - "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear.", + "description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear.", "data": { "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", From c7fc51cfa5f53a4d866aae195bface626fa9ebc8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 4 Nov 2022 00:29:52 +0000 Subject: [PATCH 218/394] [ci skip] Translation update --- .../components/acmeda/translations/he.json | 2 +- .../components/airq/translations/en.json | 39 +++--- .../components/airq/translations/hr.json | 22 ++++ .../components/airq/translations/id.json | 22 ++++ .../airthings_ble/translations/he.json | 2 +- .../ambient_station/translations/hr.json | 11 ++ .../components/apple_tv/translations/he.json | 4 +- .../components/aranet/translations/he.json | 22 ++++ .../components/aranet/translations/hr.json | 22 ++++ .../components/aranet/translations/id.json | 1 + .../components/auth/translations/hr.json | 29 +++++ .../components/awair/translations/he.json | 2 +- .../bluemaestro/translations/he.json | 2 +- .../components/braviatv/translations/id.json | 4 +- .../components/bthome/translations/he.json | 2 +- .../devolo_home_network/translations/hr.json | 14 +++ .../components/dlna_dms/translations/he.json | 2 +- .../components/elkm1/translations/hr.json | 8 ++ .../components/escea/translations/he.json | 8 ++ .../components/esphome/translations/hr.json | 17 +++ .../fireservicerota/translations/he.json | 5 + .../fireservicerota/translations/hr.json | 12 ++ .../fjaraskupan/translations/he.json | 2 +- .../components/flux_led/translations/he.json | 2 +- .../forecast_solar/translations/en.json | 2 +- .../forecast_solar/translations/id.json | 2 +- .../components/fritzbox/translations/he.json | 2 +- .../fritzbox_callmonitor/translations/he.json | 2 +- .../components/generic/translations/he.json | 2 +- .../components/generic/translations/hr.json | 32 +++++ .../components/glances/translations/en.json | 13 +- .../google_travel_time/translations/he.json | 1 + .../google_travel_time/translations/hr.json | 7 ++ .../components/govee_ble/translations/he.json | 2 +- .../components/gree/translations/he.json | 2 +- .../components/hangouts/translations/hr.json | 12 ++ .../components/hassio/translations/he.json | 111 ++++++++++++++++++ .../components/hassio/translations/hr.json | 87 ++++++++++++++ .../components/hassio/translations/pl.json | 104 +++++++++++++++- .../hisense_aehw4a1/translations/he.json | 2 +- .../homeassistant/translations/he.json | 5 +- .../homematicip_cloud/translations/hr.json | 1 + .../huawei_lte/translations/hr.json | 17 +++ .../components/hue/translations/hr.json | 11 +- .../components/icloud/translations/he.json | 6 +- .../components/ifttt/translations/hr.json | 10 ++ .../components/inkbird/translations/he.json | 2 +- .../intellifire/translations/id.json | 2 +- .../components/isy994/translations/he.json | 2 +- .../components/izone/translations/he.json | 2 +- .../components/kegtron/translations/he.json | 2 +- .../components/kulersky/translations/he.json | 2 +- .../lacrosse_view/translations/he.json | 3 +- .../components/lametric/translations/he.json | 11 ++ .../components/lametric/translations/hr.json | 7 ++ .../lametric/translations/select.hr.json | 8 ++ .../landisgyr_heat_meter/translations/he.json | 23 ++++ .../components/led_ble/translations/he.json | 3 +- .../components/lifx/translations/he.json | 2 +- .../components/lifx/translations/hr.json | 13 ++ .../components/lookin/translations/he.json | 4 +- .../lutron_caseta/translations/he.json | 2 +- .../components/mailgun/translations/hr.json | 10 ++ .../components/mazda/translations/he.json | 2 +- .../meteoclimatic/translations/he.json | 2 +- .../components/moat/translations/he.json | 2 +- .../components/mqtt/translations/hr.json | 65 ++++++++++ .../components/mqtt/translations/id.json | 9 ++ .../components/mullvad/translations/he.json | 2 +- .../components/nest/translations/hr.json | 7 ++ .../nibe_heatpump/translations/id.json | 13 +- .../components/onvif/translations/he.json | 2 +- .../openexchangerates/translations/he.json | 22 ++++ .../components/openuv/translations/hr.json | 7 +- .../components/oralb/translations/he.json | 2 +- .../components/oralb/translations/hr.json | 22 ++++ .../components/oralb/translations/id.json | 22 ++++ .../components/overkiz/translations/hr.json | 7 ++ .../ovo_energy/translations/id.json | 1 + .../plugwise/translations/select.hr.json | 17 +++ .../plugwise/translations/select.id.json | 6 + .../components/ps4/translations/he.json | 2 +- .../pushbullet/translations/bg.json | 25 ++++ .../pushbullet/translations/et.json | 25 ++++ .../pushbullet/translations/he.json | 19 +++ .../pushbullet/translations/hr.json | 24 ++++ .../pushbullet/translations/id.json | 25 ++++ .../pushbullet/translations/no.json | 25 ++++ .../pushbullet/translations/pl.json | 25 ++++ .../pushbullet/translations/zh-Hant.json | 25 ++++ .../components/pushover/translations/he.json | 13 ++ .../components/pushover/translations/id.json | 4 + .../components/qingping/translations/he.json | 2 +- .../rainmachine/translations/id.json | 1 + .../components/scrape/translations/id.json | 6 + .../components/sensibo/translations/id.json | 4 +- .../sensibo/translations/sensor.hr.json | 9 ++ .../sensibo/translations/sensor.id.json | 5 + .../components/sensor/translations/id.json | 2 + .../components/sensorpro/translations/he.json | 2 +- .../sensorpush/translations/he.json | 2 +- .../simplisafe/translations/hr.json | 12 ++ .../components/skybell/translations/he.json | 3 +- .../components/smhi/translations/hr.json | 16 +++ .../components/snooz/translations/he.json | 2 +- .../components/snooz/translations/hr.json | 21 ++++ .../components/songpal/translations/he.json | 2 +- .../components/sonos/translations/he.json | 2 +- .../soundtouch/translations/id.json | 2 +- .../statistics/translations/he.json | 3 +- .../statistics/translations/id.json | 12 ++ .../steam_online/translations/bg.json | 1 + .../components/steamist/translations/he.json | 2 +- .../switcher_kis/translations/he.json | 2 +- .../components/tautulli/translations/hr.json | 11 ++ .../tellduslive/translations/hr.json | 14 +++ .../thermobeacon/translations/he.json | 2 +- .../components/thermopro/translations/he.json | 5 + .../components/tilt_ble/translations/he.json | 2 +- .../components/tplink/translations/he.json | 2 +- .../components/tradfri/translations/hr.json | 11 +- .../transmission/translations/id.json | 13 ++ .../components/twilio/translations/hr.json | 10 ++ .../components/unifi/translations/hr.json | 10 +- .../components/upcloud/translations/id.json | 2 +- .../components/upnp/translations/he.json | 2 +- .../components/upnp/translations/hr.json | 11 ++ .../utility_meter/translations/hr.json | 11 ++ .../utility_meter/translations/id.json | 2 +- .../volvooncall/translations/he.json | 2 + .../volvooncall/translations/id.json | 2 +- .../components/wemo/translations/he.json | 2 +- .../components/wiz/translations/he.json | 2 +- .../wolflink/translations/sensor.hr.json | 7 ++ .../xiaomi_ble/translations/he.json | 2 +- .../xiaomi_miio/translations/hr.json | 7 ++ .../xiaomi_miio/translations/id.json | 5 +- .../yalexs_ble/translations/he.json | 2 +- .../components/yeelight/translations/he.json | 2 +- .../components/zamg/translations/hr.json | 12 ++ .../components/zamg/translations/id.json | 26 ++++ .../components/zerproc/translations/he.json | 2 +- .../components/zha/translations/he.json | 3 + .../components/zha/translations/hr.json | 36 ++++++ .../components/zha/translations/id.json | 8 +- 145 files changed, 1443 insertions(+), 107 deletions(-) create mode 100644 homeassistant/components/airq/translations/hr.json create mode 100644 homeassistant/components/airq/translations/id.json create mode 100644 homeassistant/components/ambient_station/translations/hr.json create mode 100644 homeassistant/components/aranet/translations/he.json create mode 100644 homeassistant/components/aranet/translations/hr.json create mode 100644 homeassistant/components/auth/translations/hr.json create mode 100644 homeassistant/components/devolo_home_network/translations/hr.json create mode 100644 homeassistant/components/elkm1/translations/hr.json create mode 100644 homeassistant/components/escea/translations/he.json create mode 100644 homeassistant/components/esphome/translations/hr.json create mode 100644 homeassistant/components/fireservicerota/translations/hr.json create mode 100644 homeassistant/components/generic/translations/hr.json create mode 100644 homeassistant/components/google_travel_time/translations/hr.json create mode 100644 homeassistant/components/hangouts/translations/hr.json create mode 100644 homeassistant/components/hassio/translations/hr.json create mode 100644 homeassistant/components/huawei_lte/translations/hr.json create mode 100644 homeassistant/components/ifttt/translations/hr.json create mode 100644 homeassistant/components/lametric/translations/hr.json create mode 100644 homeassistant/components/lametric/translations/select.hr.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/he.json create mode 100644 homeassistant/components/lifx/translations/hr.json create mode 100644 homeassistant/components/mailgun/translations/hr.json create mode 100644 homeassistant/components/openexchangerates/translations/he.json create mode 100644 homeassistant/components/oralb/translations/hr.json create mode 100644 homeassistant/components/oralb/translations/id.json create mode 100644 homeassistant/components/overkiz/translations/hr.json create mode 100644 homeassistant/components/plugwise/translations/select.hr.json create mode 100644 homeassistant/components/pushbullet/translations/bg.json create mode 100644 homeassistant/components/pushbullet/translations/et.json create mode 100644 homeassistant/components/pushbullet/translations/he.json create mode 100644 homeassistant/components/pushbullet/translations/hr.json create mode 100644 homeassistant/components/pushbullet/translations/id.json create mode 100644 homeassistant/components/pushbullet/translations/no.json create mode 100644 homeassistant/components/pushbullet/translations/pl.json create mode 100644 homeassistant/components/pushbullet/translations/zh-Hant.json create mode 100644 homeassistant/components/sensibo/translations/sensor.hr.json create mode 100644 homeassistant/components/simplisafe/translations/hr.json create mode 100644 homeassistant/components/smhi/translations/hr.json create mode 100644 homeassistant/components/snooz/translations/hr.json create mode 100644 homeassistant/components/statistics/translations/id.json create mode 100644 homeassistant/components/tautulli/translations/hr.json create mode 100644 homeassistant/components/tellduslive/translations/hr.json create mode 100644 homeassistant/components/twilio/translations/hr.json create mode 100644 homeassistant/components/utility_meter/translations/hr.json create mode 100644 homeassistant/components/wolflink/translations/sensor.hr.json create mode 100644 homeassistant/components/xiaomi_miio/translations/hr.json create mode 100644 homeassistant/components/zamg/translations/hr.json create mode 100644 homeassistant/components/zamg/translations/id.json create mode 100644 homeassistant/components/zha/translations/hr.json diff --git a/homeassistant/components/acmeda/translations/he.json b/homeassistant/components/acmeda/translations/he.json index 498f322a7b0dd9..15b5629aa5208f 100644 --- a/homeassistant/components/acmeda/translations/he.json +++ b/homeassistant/components/acmeda/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "step": { "user": { diff --git a/homeassistant/components/airq/translations/en.json b/homeassistant/components/airq/translations/en.json index 81b8c1ff83e622..c8ae857d10dbce 100644 --- a/homeassistant/components/airq/translations/en.json +++ b/homeassistant/components/airq/translations/en.json @@ -1,23 +1,22 @@ { - "config": { - "step": { - "user": { - "title": "Identify the device", - "description": "Provide the IP address or mDNS of the device and its password", - "data": { - "ip_address": "Device IP or mDNS (e.g. '123ab_air-q.local')", - "password": "Password" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_input": "Invalid hostname or IP address" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address", + "password": "Password" + }, + "description": "Provide the IP address or mDNS of the device and its password", + "title": "Identify the device" + } } - } - }, - "error": { - "cannot_connect": "Failed to connect, please check the IP or mDNS", - "invalid_auth": "Wrong password", - "invalid_input": "Invalid IP address or mDNS " - }, - "abort": { - "already_configured": "This device is already configured and available to Home Assistant" } - } -} - +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/hr.json b/homeassistant/components/airq/translations/hr.json new file mode 100644 index 00000000000000..aedbaf7ed09879 --- /dev/null +++ b/homeassistant/components/airq/translations/hr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran" + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo", + "invalid_auth": "Neva\u017ee\u0107a provjera autenti\u010dnosti", + "invalid_input": "Neva\u017ee\u0107i naziv hosta ili IP adresa" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa", + "password": "Lozinka" + }, + "description": "Navedite IP adresu ili mDNS ure\u0111aja i njegovu lozinku", + "title": "Identificirajte ure\u0111aj" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/id.json b/homeassistant/components/airq/translations/id.json new file mode 100644 index 00000000000000..db210a13491e01 --- /dev/null +++ b/homeassistant/components/airq/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_input": "Nama host atau alamat IP tidak valid" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP", + "password": "Kata Sandi" + }, + "description": "Berikan alamat IP atau mDNS perangkat dan kata sandinya", + "title": "Identifikasi perangkat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/he.json b/homeassistant/components/airthings_ble/translations/he.json index 3ba358c44651bd..467b7ec0499dd0 100644 --- a/homeassistant/components/airthings_ble/translations/he.json +++ b/homeassistant/components/airthings_ble/translations/he.json @@ -4,7 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name}", diff --git a/homeassistant/components/ambient_station/translations/hr.json b/homeassistant/components/ambient_station/translations/hr.json new file mode 100644 index 00000000000000..b4c376c38552cb --- /dev/null +++ b/homeassistant/components/ambient_station/translations/hr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API klju\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 26ec85e8dc7d92..3af73091d3e6c4 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -4,14 +4,14 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "ipv6_not_supported": "IPv6 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da.", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name} ({type})", diff --git a/homeassistant/components/aranet/translations/he.json b/homeassistant/components/aranet/translations/he.json new file mode 100644 index 00000000000000..b4afd666d4064b --- /dev/null +++ b/homeassistant/components/aranet/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/hr.json b/homeassistant/components/aranet/translations/hr.json new file mode 100644 index 00000000000000..ccdbb4f1906f92 --- /dev/null +++ b/homeassistant/components/aranet/translations/hr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran" + }, + "error": { + "unknown": "Neo\u010dekivana gre\u0161ka" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u017delite li postaviti {name}?" + }, + "user": { + "data": { + "address": "Ure\u0111aj" + }, + "description": "Odaberite ure\u0111aj za postavljanje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/id.json b/homeassistant/components/aranet/translations/id.json index 04e65296a12c3f..c41c46f5138114 100644 --- a/homeassistant/components/aranet/translations/id.json +++ b/homeassistant/components/aranet/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "integrations_diabled": "Perangkat ini tidak memiliki integrasi yang diaktifkan. Aktifkan integrasi rumah cerdas menggunakan aplikasi dan coba lagi.", "no_devices_found": "Tidak ditemukan perangkat Aranet yang tidak dikonfigurasi.", "outdated_version": "Perangkat ini menggunakan firmware usang. Perbarui setidaknya ke firmware v1.2.0 dan coba lagi." }, diff --git a/homeassistant/components/auth/translations/hr.json b/homeassistant/components/auth/translations/hr.json new file mode 100644 index 00000000000000..548b2bfdb4fe7e --- /dev/null +++ b/homeassistant/components/auth/translations/hr.json @@ -0,0 +1,29 @@ +{ + "mfa_setup": { + "notify": { + "abort": { + "no_available_service": "Nema dostupnih usluga obavijesti." + }, + "error": { + "invalid_code": "Kod nije valjan, poku\u0161ajte ponovo." + }, + "step": { + "init": { + "description": "Odaberite jednu od usluga obavijesti:", + "title": "Postavite jednokratnu lozinku koju isporu\u010duje komponenta obavijesti" + }, + "setup": { + "title": "Provjera postavki" + } + } + }, + "totp": { + "step": { + "init": { + "title": "Postavite dvofaktorsku autentifikaciju pomo\u0107u TOTP-a" + } + }, + "title": "TOTP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json index 56e562de0c5a3a..387e7fe9a62eaf 100644 --- a/homeassistant/components/awair/translations/he.json +++ b/homeassistant/components/awair/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { diff --git a/homeassistant/components/bluemaestro/translations/he.json b/homeassistant/components/bluemaestro/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/bluemaestro/translations/he.json +++ b/homeassistant/components/bluemaestro/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index 853dd7da29edb7..61b5b17ff1e316 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -19,7 +19,7 @@ "pin": "Kode PIN", "use_psk": "Gunakan autentikasi PSK" }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN.", + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN.", "title": "Otorisasi TV Sony Bravia" }, "confirm": { @@ -30,7 +30,7 @@ "pin": "Kode PIN", "use_psk": "Gunakan autentikasi PSK" }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN." + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN." }, "user": { "data": { diff --git a/homeassistant/components/bthome/translations/he.json b/homeassistant/components/bthome/translations/he.json index b90a366130ab0c..0df85dd1fe5d4a 100644 --- a/homeassistant/components/bthome/translations/he.json +++ b/homeassistant/components/bthome/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "flow_title": "{name}", diff --git a/homeassistant/components/devolo_home_network/translations/hr.json b/homeassistant/components/devolo_home_network/translations/hr.json new file mode 100644 index 00000000000000..3e7836e5961fdd --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/hr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponovna provjera autenti\u010dnosti je uspje\u0161na" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Lozinka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/he.json b/homeassistant/components/dlna_dms/translations/he.json index cfe995b892124f..41bb4ddbf8b1fc 100644 --- a/homeassistant/components/dlna_dms/translations/he.json +++ b/homeassistant/components/dlna_dms/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/elkm1/translations/hr.json b/homeassistant/components/elkm1/translations/hr.json new file mode 100644 index 00000000000000..06224788ca621d --- /dev/null +++ b/homeassistant/components/elkm1/translations/hr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "invalid_auth": "Neva\u017ee\u0107a provjera autenti\u010dnosti", + "unknown": "Neo\u010dekivana gre\u0161ka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/he.json b/homeassistant/components/escea/translations/he.json new file mode 100644 index 00000000000000..032c9c9fa17f7a --- /dev/null +++ b/homeassistant/components/escea/translations/he.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/hr.json b/homeassistant/components/esphome/translations/hr.json new file mode 100644 index 00000000000000..5984e832e28ef9 --- /dev/null +++ b/homeassistant/components/esphome/translations/hr.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "authenticate": { + "data": { + "password": "Lozinka" + } + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/he.json b/homeassistant/components/fireservicerota/translations/he.json index 61dee20d1ce485..313b46455f2c80 100644 --- a/homeassistant/components/fireservicerota/translations/he.json +++ b/homeassistant/components/fireservicerota/translations/he.json @@ -16,6 +16,11 @@ "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } }, + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/fireservicerota/translations/hr.json b/homeassistant/components/fireservicerota/translations/hr.json new file mode 100644 index 00000000000000..e1fd87e1f7688f --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/hr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Lozinka" + }, + "description": "Tokeni za provjeru autenti\u010dnosti postali su neva\u017ee\u0107i, prijavite se da ih ponovno izradite." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/he.json b/homeassistant/components/fjaraskupan/translations/he.json index 380dbc5d7fcdc7..032c9c9fa17f7a 100644 --- a/homeassistant/components/fjaraskupan/translations/he.json +++ b/homeassistant/components/fjaraskupan/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/flux_led/translations/he.json b/homeassistant/components/flux_led/translations/he.json index aa2d7877791bf9..d8290fd672b2c6 100644 --- a/homeassistant/components/flux_led/translations/he.json +++ b/homeassistant/components/flux_led/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json index 2aa5a37cd1c992..56c210a7c19897 100644 --- a/homeassistant/components/forecast_solar/translations/en.json +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -25,7 +25,7 @@ "inverter_size": "Inverter size (Watt)", "modules power": "Total Watt peak power of your solar modules" }, - "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear." + "description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear." } } } diff --git a/homeassistant/components/forecast_solar/translations/id.json b/homeassistant/components/forecast_solar/translations/id.json index 5bd1236d6a62df..9c30668d1de968 100644 --- a/homeassistant/components/forecast_solar/translations/id.json +++ b/homeassistant/components/forecast_solar/translations/id.json @@ -25,7 +25,7 @@ "inverter_size": "Ukuran inverter (Watt)", "modules power": "Total daya puncak modul surya Anda dalam Watt" }, - "description": "Nilai-nilai ini memungkinkan penyesuaian hasil Solar.Forecast. Rujuk ke dokumentasi jika bidang isian tidak jelas." + "description": "Nilai-nilai ini memungkinkan penyesuaian hasil Forecast.Solar. Rujuk ke dokumentasi jika bidang isian tidak jelas." } } } diff --git a/homeassistant/components/fritzbox/translations/he.json b/homeassistant/components/fritzbox/translations/he.json index ec9248b5ea63d6..c2c0d4be01541d 100644 --- a/homeassistant/components/fritzbox/translations/he.json +++ b/homeassistant/components/fritzbox/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/he.json b/homeassistant/components/fritzbox_callmonitor/translations/he.json index 7951a71054c9b8..fc8b733cb1aa9f 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/he.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" diff --git a/homeassistant/components/generic/translations/he.json b/homeassistant/components/generic/translations/he.json index d4e2e4958553a1..6eaec7b7c9d4a4 100644 --- a/homeassistant/components/generic/translations/he.json +++ b/homeassistant/components/generic/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { diff --git a/homeassistant/components/generic/translations/hr.json b/homeassistant/components/generic/translations/hr.json new file mode 100644 index 00000000000000..2dd06c640c9844 --- /dev/null +++ b/homeassistant/components/generic/translations/hr.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "user_confirm_still": { + "data": { + "confirmed_ok": "Ova slika izgleda dobro." + }, + "description": "![Pregled fotografije s kamere]( {preview_url} )", + "title": "Pregled" + } + } + }, + "options": { + "step": { + "confirm_still": { + "data": { + "confirmed_ok": "Ova slika izgleda dobro." + }, + "description": "![Pregled fotografije s kamere]({preview_url})", + "title": "Pregled" + }, + "init": { + "data": { + "password": "Lozinka", + "rtsp_transport": "RTSP transportni protokol", + "username": "Korisni\u010dko ime", + "verify_ssl": "Provjera SSL certifikata" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json index 726e4716224580..87c53c3cf48a2f 100644 --- a/homeassistant/components/glances/translations/en.json +++ b/homeassistant/components/glances/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "wrong_version": "Version not supported (2 or 3 only)" }, "step": { "user": { @@ -21,5 +22,15 @@ "title": "Setup Glances" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequency" + }, + "description": "Configure options for Glances" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/he.json b/homeassistant/components/google_travel_time/translations/he.json index b10c8890fd425f..3f8d7bd125759c 100644 --- a/homeassistant/components/google_travel_time/translations/he.json +++ b/homeassistant/components/google_travel_time/translations/he.json @@ -24,6 +24,7 @@ "data": { "language": "\u05e9\u05e4\u05d4", "time": "\u05d6\u05de\u05df", + "traffic_mode": "\u05de\u05e6\u05d1 \u05ea\u05e0\u05d5\u05e2\u05d4", "units": "\u05d9\u05d7\u05d9\u05d3\u05d5\u05ea" } } diff --git a/homeassistant/components/google_travel_time/translations/hr.json b/homeassistant/components/google_travel_time/translations/hr.json new file mode 100644 index 00000000000000..6e71d340dc1ee5 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Neva\u017ee\u0107a provjera autenti\u010dnosti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/he.json b/homeassistant/components/govee_ble/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/govee_ble/translations/he.json +++ b/homeassistant/components/govee_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/gree/translations/he.json b/homeassistant/components/gree/translations/he.json index d3d68dccc93cc8..4eafc6dc29bd36 100644 --- a/homeassistant/components/gree/translations/he.json +++ b/homeassistant/components/gree/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/hangouts/translations/hr.json b/homeassistant/components/hangouts/translations/hr.json new file mode 100644 index 00000000000000..59b98ae9ae96b4 --- /dev/null +++ b/homeassistant/components/hangouts/translations/hr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "Lozinka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/he.json b/homeassistant/components/hassio/translations/he.json index 8926338221ad8e..13c9cfd949c790 100644 --- a/homeassistant/components/hassio/translations/he.json +++ b/homeassistant/components/hassio/translations/he.json @@ -1,6 +1,117 @@ { + "issues": { + "unhealthy": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05db\u05e8\u05d2\u05e2 \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05d1\u05d2\u05dc\u05dc {reason}. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - {reason}" + }, + "unhealthy_docker": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05ea\u05e7\u05d9\u05e0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea Docker \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - Docker \u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9" + }, + "unhealthy_privileged": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d0\u05d9\u05df \u05dc\u05d4 \u05d2\u05d9\u05e9\u05d4 \u05de\u05d5\u05e8\u05e9\u05d9\u05ea \u05dc\u05d6\u05de\u05df \u05d4\u05e8\u05d9\u05e6\u05d4 \u05e9\u05dc docker. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05dc\u05d0 \u05de\u05d9\u05d5\u05d7\u05e1" + }, + "unhealthy_setup": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d4\u05ea\u05e7\u05e0\u05d4 \u05dc\u05d0 \u05d4\u05d5\u05e9\u05dc\u05de\u05d4. \u05d9\u05e9\u05e0\u05df \u05de\u05e1\u05e4\u05e8 \u05e1\u05d9\u05d1\u05d5\u05ea \u05dc\u05db\u05da \u05e9\u05d6\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05d4\u05ea\u05e8\u05d7\u05e9, \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05d4\u05d4\u05ea\u05e7\u05e0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "unhealthy_supervisor": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05de\u05e4\u05e7\u05d7 \u05dc\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05e2\u05d3\u05db\u05e0\u05d9\u05ea \u05d1\u05d9\u05d5\u05ea\u05e8 \u05e0\u05db\u05e9\u05dc. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05e2\u05d3\u05db\u05d5\u05df \u05d4\u05de\u05e4\u05e7\u05d7 \u05e0\u05db\u05e9\u05dc" + }, + "unhealthy_untrusted": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d1\u05e8\u05d9\u05d0\u05d4 \u05db\u05e2\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d9\u05d0 \u05d6\u05d9\u05d4\u05ea\u05d4 \u05e7\u05d5\u05d3 \u05d0\u05d5 \u05ea\u05de\u05d5\u05e0\u05d5\u05ea \u05dc\u05d0 \u05de\u05d4\u05d9\u05de\u05e0\u05d9\u05dd \u05d1\u05e9\u05d9\u05de\u05d5\u05e9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0\u05d4 - \u05e7\u05d5\u05d3 \u05dc\u05d0 \u05de\u05d4\u05d9\u05de\u05df" + }, + "unsupported": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05e2\u05e7\u05d1 {reason}. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - {reason}" + }, + "unsupported_apparmor": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-AppArmor \u05e4\u05d5\u05e2\u05dc \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9 \u05d5\u05d4\u05e8\u05d7\u05d1\u05d5\u05ea \u05e4\u05d5\u05e2\u05dc\u05d5\u05ea \u05d1\u05e6\u05d5\u05e8\u05d4 \u05dc\u05d0 \u05de\u05d5\u05d2\u05e0\u05ea \u05d5\u05dc\u05d0 \u05de\u05d0\u05d5\u05d1\u05d8\u05d7\u05ea. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea AppArmor" + }, + "unsupported_cgroup_version": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05dc\u05d0 \u05e0\u05db\u05d5\u05e0\u05d4 \u05e9\u05dc Docker CGroup \u05e0\u05de\u05e6\u05d0\u05ea \u05d1\u05e9\u05d9\u05de\u05d5\u05e9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05d0\u05d5\u05d3\u05d5\u05ea \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05e0\u05db\u05d5\u05e0\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d2\u05e8\u05e1\u05ea CGroup" + }, + "unsupported_connectivity_check": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05d9\u05db\u05d5\u05dc \u05dc\u05e7\u05d1\u05d5\u05e2 \u05de\u05ea\u05d9 \u05d7\u05d9\u05d1\u05d5\u05e8 \u05dc\u05d0\u05d9\u05e0\u05d8\u05e8\u05e0\u05d8 \u05d6\u05de\u05d9\u05df. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05d3\u05d9\u05e7\u05ea \u05e7\u05d9\u05e9\u05d5\u05e8\u05d9\u05d5\u05ea \u05de\u05d5\u05e9\u05d1\u05ea\u05ea" + }, + "unsupported_content_trust": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Home Assistant \u05d0\u05d9\u05e0\u05d5 \u05d9\u05db\u05d5\u05dc \u05dc\u05d0\u05de\u05ea \u05e9\u05d4\u05ea\u05d5\u05db\u05df \u05d4\u05de\u05d5\u05e4\u05e2\u05dc \u05d4\u05d5\u05d0 \u05de\u05d4\u05d9\u05de\u05df \u05d5\u05d0\u05d9\u05e0\u05d5 \u05e9\u05d5\u05e0\u05d4 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05ea\u05d5\u05e7\u05e4\u05d9\u05dd. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05d3\u05d9\u05e7\u05ea \u05d0\u05de\u05d5\u05df \u05ea\u05d5\u05db\u05df \u05de\u05d5\u05e9\u05d1\u05ea" + }, + "unsupported_dbus": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-D-Bus \u05e4\u05d5\u05e2\u05dc \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d3\u05d1\u05e8\u05d9\u05dd \u05e8\u05d1\u05d9\u05dd \u05e0\u05db\u05e9\u05dc\u05d9\u05dd \u05dc\u05dc\u05d0 \u05d6\u05d4 \u05db\u05d2\u05d5\u05df \u05d4\u05de\u05e4\u05e7\u05d7 \u05d0\u05d9\u05e0\u05d5 \u05d9\u05db\u05d5\u05dc \u05dc\u05ea\u05e7\u05e9\u05e8 \u05e2\u05dd \u05d4\u05de\u05d0\u05e8\u05d7. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea D-Bus" + }, + "unsupported_dns_server": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05e9\u05e8\u05ea \u05d4-DNS \u05e9\u05e1\u05d5\u05e4\u05e7 \u05d0\u05d9\u05e0\u05d5 \u05e4\u05d5\u05e2\u05dc \u05db\u05e8\u05d0\u05d5\u05d9 \u05d5\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05d4-DNS \u05d4\u05d7\u05d5\u05d6\u05e8\u05ea \u05d4\u05e4\u05db\u05d4 \u05dc\u05dc\u05d0 \u05d6\u05de\u05d9\u05e0\u05d4. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e9\u05e8\u05ea DNS" + }, + "unsupported_docker_configuration": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4-Docker daemon \u05e4\u05d5\u05e2\u05dc \u05d1\u05d0\u05d5\u05e4\u05df \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - Docker \u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9" + }, + "unsupported_docker_version": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05dc\u05d0 \u05e0\u05db\u05d5\u05e0\u05d4 \u05e9\u05dc Docker \u05e0\u05de\u05e6\u05d0\u05ea \u05d1\u05e9\u05d9\u05de\u05d5\u05e9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05de\u05d4\u05d9 \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05e0\u05db\u05d5\u05e0\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d2\u05e8\u05e1\u05ea Docker" + }, + "unsupported_job_conditions": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05ea\u05e0\u05d0\u05d9 \u05e2\u05d1\u05d5\u05d3\u05d4 \u05d0\u05d7\u05d3 \u05d0\u05d5 \u05d9\u05d5\u05ea\u05e8 \u05d4\u05d5\u05e9\u05d1\u05ea\u05d5 \u05d0\u05e9\u05e8 \u05de\u05d2\u05e0\u05d9\u05dd \u05de\u05e4\u05e0\u05d9 \u05db\u05e9\u05dc\u05d9\u05dd \u05d5\u05e9\u05d1\u05e8\u05d9\u05dd \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d9\u05dd. \u05d9\u05e9 \u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d4\u05d4\u05d2\u05e0\u05d5\u05ea \u05de\u05d5\u05e9\u05d1\u05ea\u05d5\u05ea" + }, + "unsupported_lxc": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d4\u05d9\u05d0 \u05de\u05d5\u05e4\u05e2\u05dc\u05ea \u05d1\u05de\u05d7\u05e9\u05d1 \u05d5\u05d9\u05e8\u05d8\u05d5\u05d0\u05dc\u05d9 LXC. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d6\u05d5\u05d4\u05ea\u05d4 LXC" + }, + "unsupported_network_manager": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05de\u05e0\u05d4\u05dc \u05d4\u05e8\u05e9\u05ea \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05de\u05e0\u05d4\u05dc \u05d4\u05e8\u05e9\u05ea" + }, + "unsupported_os": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d4\u05e4\u05e2\u05dc\u05d4 \u05e9\u05d1\u05e9\u05d9\u05de\u05d5\u05e9 \u05d0\u05d9\u05e0\u05d4 \u05e0\u05d1\u05d3\u05e7\u05ea \u05d0\u05d5 \u05de\u05ea\u05d5\u05d7\u05d6\u05e7\u05ea \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05e2\u05dd \u05d4\u05de\u05e4\u05e7\u05d7. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05dc\u05d5 \u05de\u05e2\u05e8\u05db\u05d5\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05e0\u05ea\u05de\u05db\u05d5\u05ea \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4" + }, + "unsupported_os_agent": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-OS-Agent \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea OS-Agent" + }, + "unsupported_restart_policy": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05dc Docker \u05e7\u05d5\u05e0\u05d8\u05d9\u05d9\u05e0\u05e8 \u05d9\u05e9 \u05e2\u05e8\u05db\u05ea \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05de\u05d7\u05d3\u05e9 \u05e9\u05e2\u05dc\u05d5\u05dc\u05d4 \u05dc\u05d2\u05e8\u05d5\u05dd \u05dc\u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05e2\u05ea \u05d4\u05d0\u05ea\u05d7\u05d5\u05dc. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05de\u05d3\u05d9\u05e0\u05d9\u05d5\u05ea \u05d4\u05e4\u05e2\u05dc\u05d4 \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e7\u05d5\u05e0\u05d8\u05d9\u05d9\u05e0\u05e8" + }, + "unsupported_software": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d6\u05d5\u05d4\u05ea\u05d4 \u05ea\u05d5\u05db\u05e0\u05d4 \u05e0\u05d5\u05e1\u05e4\u05ea \u05de\u05d7\u05d5\u05e5 \u05dc\u05de\u05e2\u05e8\u05db\u05ea \u05d4\u05d0\u05e7\u05d5\u05dc\u05d5\u05d2\u05d9\u05ea \u05e9\u05dc Home Assistant. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05ea\u05d5\u05db\u05e0\u05d4 \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea" + }, + "unsupported_source_mods": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05e7\u05d5\u05d3 \u05d4\u05de\u05e7\u05d5\u05e8 \u05e9\u05dc \u05d4\u05de\u05e4\u05e7\u05d7 \u05d4\u05e9\u05ea\u05e0\u05d4. \u05d9\u05e9 \u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05de\u05e7\u05d5\u05e8 \u05d4\u05de\u05e4\u05e7\u05d7" + }, + "unsupported_supervisor_version": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9\u05d2\u05d9\u05e8\u05e1\u05d4 \u05dc\u05d0 \u05de\u05e2\u05d5\u05d3\u05db\u05e0\u05ea \u05e9\u05dc \u05de\u05e4\u05e7\u05d7 \u05e0\u05de\u05e6\u05d0\u05ea \u05d1\u05e9\u05d9\u05de\u05d5\u05e9 \u05d5\u05e2\u05d3\u05db\u05d5\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9 \u05d4\u05d5\u05e9\u05d1\u05ea. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d2\u05e8\u05e1\u05ea \u05de\u05e4\u05e7\u05d7" + }, + "unsupported_systemd": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Systemd \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05d1\u05de\u05e2\u05e8\u05db\u05ea" + }, + "unsupported_systemd_journal": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Systemd Journal \u05d5/\u05d0\u05d5 \u05e9\u05d9\u05e8\u05d5\u05ea \u05d4\u05e9\u05e2\u05e8 \u05d7\u05e1\u05e8\u05d9\u05dd, \u05d0\u05d9\u05e0\u05dd \u05e4\u05e2\u05d9\u05dc\u05d9\u05dd \u05d0\u05d5 \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd \u05d1\u05e6\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05dc\u05de\u05d5\u05d3 \u05e2\u05d5\u05d3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05e9\u05dc Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "\u05d4\u05de\u05e2\u05e8\u05db\u05ea \u05d0\u05d9\u05e0\u05d4 \u05e0\u05ea\u05de\u05db\u05ea \u05de\u05db\u05d9\u05d5\u05d5\u05df \u05e9-Systemd Resolved \u05d7\u05e1\u05e8, \u05dc\u05d0 \u05e4\u05e2\u05d9\u05dc \u05d0\u05d5 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05e9\u05d2\u05d5\u05d9. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e7\u05d9\u05e9\u05d5\u05e8 \u05db\u05d3\u05d9 \u05dc\u05e7\u05d1\u05dc \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05ea\u05e7\u05df \u05d6\u05d0\u05ea.", + "title": "\u05de\u05e2\u05e8\u05db\u05ea \u05dc\u05d0 \u05e0\u05ea\u05de\u05db\u05ea - \u05d1\u05e2\u05d9\u05d5\u05ea \u05e9\u05e0\u05e4\u05ea\u05e8\u05d5 \u05e2\u05dc \u05d9\u05d3\u05d9 \u05d4\u05de\u05e2\u05e8\u05db\u05ea" + } + }, "system_health": { "info": { + "agent_version": "\u05d2\u05e8\u05e1\u05ea \u05d4\u05e1\u05d5\u05db\u05df", "board": "\u05dc\u05d5\u05d7", "disk_total": "\u05e1\u05d4\"\u05db \u05d3\u05d9\u05e1\u05e7", "disk_used": "\u05d3\u05d9\u05e1\u05e7 \u05d1\u05e9\u05d9\u05de\u05d5\u05e9", diff --git a/homeassistant/components/hassio/translations/hr.json b/homeassistant/components/hassio/translations/hr.json new file mode 100644 index 00000000000000..c028223588b19e --- /dev/null +++ b/homeassistant/components/hassio/translations/hr.json @@ -0,0 +1,87 @@ +{ + "issues": { + "unhealthy": { + "description": "Sustav je trenutno nezdrav zbog {reason}. Sustav nije podr\u017ean zbog {reason}. Koristite vezu da saznate vi\u0161e i kako to popraviti.", + "title": "Nezdrav sustav - {reason}" + }, + "unhealthy_docker": { + "title": "Nezdrav sustav - Docker je pogre\u0161no konfiguriran" + }, + "unhealthy_privileged": { + "title": "Nezdrav sustav - Nije privilegiran" + }, + "unhealthy_setup": { + "title": "Neispravan sustav - Postavljanje nije uspjelo" + }, + "unhealthy_supervisor": { + "title": "Nezdravi sustav - A\u017euriranje Supervisora nije uspjelo" + }, + "unhealthy_untrusted": { + "title": "Nezdravi sustav - Nepouzdan kod" + }, + "unsupported": { + "description": "Sustav nije podr\u017ean zbog {reason}. Koristite vezu da saznate vi\u0161e i kako to popraviti.", + "title": "Nepodr\u017eani sustav - {reason}" + }, + "unsupported_apparmor": { + "title": "Nepodr\u017eani sustav - Problemi s AppArmorom" + }, + "unsupported_cgroup_version": { + "title": "Nepodr\u017eani sustav - verzija CGroup" + }, + "unsupported_connectivity_check": { + "title": "Nepodr\u017eani sustav \u2013 Provjera povezivosti je onemogu\u0107ena" + }, + "unsupported_content_trust": { + "title": "Nepodr\u017eani sustav - Provjera pouzdanosti sadr\u017eaja onemogu\u0107ena" + }, + "unsupported_dbus": { + "title": "Nepodr\u017eani sustav - D-Bus problemi" + }, + "unsupported_dns_server": { + "title": "Nepodr\u017eani sustav - Problemi s DNS poslu\u017eiteljem" + }, + "unsupported_docker_configuration": { + "title": "Nepodr\u017eani sustav - Docker je pogre\u0161no konfiguriran" + }, + "unsupported_docker_version": { + "title": "Nepodr\u017eani sustav - Docker verzija" + }, + "unsupported_job_conditions": { + "title": "Nepodr\u017eani sustav - Za\u0161tite onemogu\u0107ene" + }, + "unsupported_lxc": { + "title": "Nepodr\u017eani sustav - LXC otkriven" + }, + "unsupported_network_manager": { + "title": "Nepodr\u017eani sustav - Problemi s upraviteljem mre\u017ee" + }, + "unsupported_os": { + "title": "Nepodr\u017eani sustav - Operativni sustav" + }, + "unsupported_os_agent": { + "title": "Nepodr\u017eani sustav - problemi s OS-Agentom" + }, + "unsupported_restart_policy": { + "title": "Nepodr\u017eani sustav - Pravila ponovnog pokretanja kontejnera" + }, + "unsupported_software": { + "title": "Nepodr\u017eani sustav - Nepodr\u017eani softver" + }, + "unsupported_source_mods": { + "title": "Nepodr\u017eani sustav - Izmjene izvornog koda supervizora" + }, + "unsupported_supervisor_version": { + "title": "Nepodr\u017eani sustav - Supervisor verzija" + }, + "unsupported_systemd": { + "title": "Nepodr\u017eani sustav - Systemd problemi" + }, + "unsupported_systemd_journal": { + "title": "Nepodr\u017eani sustav - Systemd Journal problemi" + }, + "unsupported_systemd_resolved": { + "title": "Nepodr\u017eani sustav - Systemd-Resolved problemi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json index 0fdbfa2ab68beb..7ee470bc9bd594 100644 --- a/homeassistant/components/hassio/translations/pl.json +++ b/homeassistant/components/hassio/translations/pl.json @@ -1,12 +1,112 @@ { "issues": { "unhealthy": { - "description": "System jest obecnie \"niezdrowy\" z powodu \u201e{reason}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej o tym, co jest nie tak i jak to naprawi\u0107.", + "description": "System jest obecnie \"niezdrowy\" z powodu \u201e{reason}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", "title": "Niezdrowy system \u2013 {reason}" }, + "unhealthy_docker": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c Docker jest niepoprawnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 b\u0142\u0119dna konfiguracja Dockera" + }, + "unhealthy_privileged": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c nie ma uprzywilejowanego dost\u0119pu do Docker runtime. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 brak uprzywilejowania" + }, + "unhealthy_setup": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c konfiguracja nie zosta\u0142a uko\u0144czona. Mo\u017ce si\u0119 tak zdarzy\u0107 z wielu powod\u00f3w, skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 konfiguracja nie powiod\u0142a si\u0119" + }, + "unhealthy_supervisor": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c pr\u00f3ba aktualizacji Supervisora do najnowszej wersji nie powiod\u0142a si\u0119. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 aktualizacja Supervisora nie powiod\u0142a si\u0119" + }, + "unhealthy_untrusted": { + "description": "System jest obecnie \"niezdrowy\", poniewa\u017c wykry\u0142 niezaufany kod lub obrazy w u\u017cyciu. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "\"Niezdrowy\" system \u2014 niezaufany kod" + }, "unsupported": { - "description": "System nie jest obs\u0142ugiwany z powodu \u201e{pow\u00f3d}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej o tym, co to oznacza i jak wr\u00f3ci\u0107 do obs\u0142ugiwanego systemu.", + "description": "System nie jest obs\u0142ugiwany z powodu \u201e{pow\u00f3d}\u201d. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", "title": "Nieobs\u0142ugiwany system \u2013 {reason}" + }, + "unsupported_apparmor": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c AppArmor dzia\u0142a nieprawid\u0142owo, a dodatki dzia\u0142aj\u0105 w spos\u00f3b niezabezpieczony i niezabezpieczony. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z AppArmor" + }, + "unsupported_cgroup_version": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywana jest niew\u0142a\u015bciwa wersja Docker CGroup. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119, jaka jest poprawna wersja i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 wersja CGroup" + }, + "unsupported_connectivity_check": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c Home Assistant nie mo\u017ce okre\u015bli\u0107, kiedy po\u0142\u0105czenie internetowe jest dost\u0119pne. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 sprawdzanie \u0142\u0105czno\u015bci wy\u0142\u0105czone" + }, + "unsupported_content_trust": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c Home Assistant nie mo\u017ce zweryfikowa\u0107, czy uruchamiana zawarto\u015b\u0107 jest zaufana i nie jest modyfikowana przez atakuj\u0105cych. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2013 kontrola zaufania do tre\u015bci wy\u0142\u0105czona" + }, + "unsupported_dbus": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c D-Bus dzia\u0142a nieprawid\u0142owo. Wiele rzeczy zawodzi bez tego, poniewa\u017c Supervisor nie mo\u017ce komunikowa\u0107 si\u0119 z hostem. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z D-Bus" + }, + "unsupported_dns_server": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c podany serwer DNS nie dzia\u0142a poprawnie, a opcja rezerwowego DNS zosta\u0142a wy\u0142\u0105czona. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z serwerem DNS" + }, + "unsupported_docker_configuration": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c Docker darmon dzia\u0142a w nieoczekiwany spos\u00f3b. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 b\u0142\u0119dna konfiguracja Dockera" + }, + "unsupported_docker_version": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywana jest niew\u0142a\u015bciwa wersja Dockera. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119, jaka jest poprawna wersja i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 wersja Dockera" + }, + "unsupported_job_conditions": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c co najmniej jeden warunek pracy zosta\u0142 wy\u0142\u0105czony, co chroni przed nieoczekiwanymi awariami i uszkodzeniami. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 ochrona wy\u0142\u0105czona" + }, + "unsupported_lxc": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c jest uruchomiony na maszynie wirtualnej LXC. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2013 wykryto LXC" + }, + "unsupported_network_manager": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Mened\u017cera Sieci, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Mened\u017cerem Sieci" + }, + "unsupported_os": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywany system operacyjny nie jest przetestowany ani wspierany do u\u017cytku z Supervisorem. Skorzystaj z linku, aby sprawdzi\u0107 obs\u0142ugiwane systemy operacyjne i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 system operacyjny" + }, + "unsupported_os_agent": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje OS-Agent, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system - problemy z OS-Agent" + }, + "unsupported_restart_policy": { + "description": "System jest nieobs\u0142ugiwany, poniewa\u017c kontener Docker ma ustawion\u0105 polityk\u0119 restartu, kt\u00f3ra mo\u017ce powodowa\u0107 problemy podczas uruchamiania. U\u017cyj linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 polityka restartu kontenera" + }, + "unsupported_software": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c wykryto dodatkowe oprogramowanie spoza ekosystemu Home Assistant. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 nieobs\u0142ugiwane oprogramowanie" + }, + "unsupported_source_mods": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c kod \u017ar\u00f3d\u0142owy Supervisora zosta\u0142 zmodyfikowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 modyfikacja kodu \u017ar\u00f3d\u0142owego Supervisora" + }, + "unsupported_supervisor_version": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c u\u017cywana jest nieaktualna wersja Supervisora, a automatyczna aktualizacja zosta\u0142a wy\u0142\u0105czona. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 wersja Supervisora" + }, + "unsupported_systemd": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Systemd, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Systemd" + }, + "unsupported_systemd_journal": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Systemd Journal i/lub us\u0142ugi bramki, jest ona nieaktywna lub b\u0142\u0119dnie skonfigurowana . Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "System nie jest obs\u0142ugiwany, poniewa\u017c brakuje Systemd Resolved, jest on nieaktywny lub b\u0142\u0119dnie skonfigurowany. Skorzystaj z linku, aby dowiedzie\u0107 si\u0119 wi\u0119cej i jak to naprawi\u0107.", + "title": "Nieobs\u0142ugiwany system \u2014 problemy z Systemd Resolved" } }, "system_health": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/he.json b/homeassistant/components/hisense_aehw4a1/translations/he.json index 380dbc5d7fcdc7..032c9c9fa17f7a 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/he.json +++ b/homeassistant/components/hisense_aehw4a1/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/homeassistant/translations/he.json b/homeassistant/components/homeassistant/translations/he.json index 0b9afe54386744..75f487a964e3fd 100644 --- a/homeassistant/components/homeassistant/translations/he.json +++ b/homeassistant/components/homeassistant/translations/he.json @@ -1,7 +1,9 @@ { "system_health": { "info": { + "arch": "\u05d0\u05e8\u05db\u05d9\u05d8\u05e7\u05d8\u05d5\u05e8\u05ea \u05de\u05e2\u05d1\u05d3", "config_dir": "\u05e1\u05e4\u05e8\u05d9\u05d9\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4", + "dev": "\u05e4\u05d9\u05ea\u05d5\u05d7", "docker": "Docker", "hassio": "\u05de\u05e4\u05e7\u05d7", "installation_type": "\u05e1\u05d5\u05d2 \u05d4\u05ea\u05e7\u05e0\u05d4", @@ -10,7 +12,8 @@ "python_version": "\u05d2\u05e8\u05e1\u05ea \u05e4\u05d9\u05d9\u05ea\u05d5\u05df", "timezone": "\u05d0\u05d6\u05d5\u05e8 \u05d6\u05de\u05df", "user": "\u05de\u05e9\u05ea\u05de\u05e9", - "version": "\u05d2\u05d9\u05e8\u05e1\u05d4" + "version": "\u05d2\u05d9\u05e8\u05e1\u05d4", + "virtualenv": "\u05e1\u05d1\u05d9\u05d1\u05d4 \u05d5\u05d9\u05e8\u05d8\u05d5\u05d0\u05dc\u05d9\u05ea" } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/hr.json b/homeassistant/components/homematicip_cloud/translations/hr.json index 648dbfe73f98e7..a1e99ac96425c2 100644 --- a/homeassistant/components/homematicip_cloud/translations/hr.json +++ b/homeassistant/components/homematicip_cloud/translations/hr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "connection_aborted": "Povezivanje nije uspjelo", "unknown": "Do\u0161lo je do nepoznate pogre\u0161ke." } } diff --git a/homeassistant/components/huawei_lte/translations/hr.json b/homeassistant/components/huawei_lte/translations/hr.json new file mode 100644 index 00000000000000..e5211ba1d01245 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/hr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponovna provjera autenti\u010dnosti je uspje\u0161na" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Lozinka", + "username": "Korisni\u010dko ime" + }, + "description": "Unesite pristupne podatke za ure\u0111aj.", + "title": "Ponovno autentificirajte integraciju" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/hr.json b/homeassistant/components/hue/translations/hr.json index aa28e012caf7ed..1fe00c2f528149 100644 --- a/homeassistant/components/hue/translations/hr.json +++ b/homeassistant/components/hue/translations/hr.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "all_configured": "Sva Philips Hue \u010dvori\u0161ta su ve\u0107 konfigurirana", + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "cannot_connect": "Povezivanje nije uspjelo", + "discover_timeout": "Nije mogu\u0107e otkriti Hue \u010dvori\u0161ta", + "no_bridges": "Nisu otkrivena Philips Hue \u010dvori\u0161ta", + "unknown": "Neo\u010dekivana gre\u0161ka" + }, "error": { "linking": "Do\u0161lo je do nepoznate pogre\u0161ke u povezivanju.", "register_failed": "Registracija nije uspjela. Poku\u0161ajte ponovo" @@ -8,7 +16,8 @@ "init": { "data": { "host": "Host" - } + }, + "title": "Odaberite Hue \u010dvori\u0161te" } } } diff --git a/homeassistant/components/icloud/translations/he.json b/homeassistant/components/icloud/translations/he.json index eae7fa97a834fd..95ed8490521026 100644 --- a/homeassistant/components/icloud/translations/he.json +++ b/homeassistant/components/icloud/translations/he.json @@ -23,10 +23,10 @@ }, "trusted_device": { "data": { - "trusted_device": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df" + "trusted_device": "\u05d4\u05ea\u05e7\u05df \u05de\u05d4\u05d9\u05de\u05df" }, - "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05d4\u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc\u05da", - "title": "\u05de\u05db\u05e9\u05d9\u05e8 \u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc iCloud" + "description": "\u05d9\u05e9 \u05dc\u05d1\u05d7\u05d5\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc\u05da", + "title": "\u05d4\u05ea\u05e7\u05df \u05de\u05d4\u05d9\u05de\u05df \u05e9\u05dc iCloud" }, "user": { "data": { diff --git a/homeassistant/components/ifttt/translations/hr.json b/homeassistant/components/ifttt/translations/hr.json new file mode 100644 index 00000000000000..9b1af1faf2d8bc --- /dev/null +++ b/homeassistant/components/ifttt/translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Jeste li sigurni da \u017eelite postaviti IFTTT?", + "title": "Postavljanje IFTTT Webhook apleta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/he.json b/homeassistant/components/inkbird/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/inkbird/translations/he.json +++ b/homeassistant/components/inkbird/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/intellifire/translations/id.json b/homeassistant/components/intellifire/translations/id.json index 6a38f5018112f2..70205984aaa012 100644 --- a/homeassistant/components/intellifire/translations/id.json +++ b/homeassistant/components/intellifire/translations/id.json @@ -19,7 +19,7 @@ } }, "dhcp_confirm": { - "description": "Ingin menyiapkan {host}\nSerial: ({serial})?" + "description": "Ingin menyiapkan {host}\nSerial: {serial}?" }, "manual_device_entry": { "data": { diff --git a/homeassistant/components/isy994/translations/he.json b/homeassistant/components/isy994/translations/he.json index e72724785cc70f..20e1da4fb79714 100644 --- a/homeassistant/components/isy994/translations/he.json +++ b/homeassistant/components/isy994/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/izone/translations/he.json b/homeassistant/components/izone/translations/he.json index 380dbc5d7fcdc7..032c9c9fa17f7a 100644 --- a/homeassistant/components/izone/translations/he.json +++ b/homeassistant/components/izone/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/kegtron/translations/he.json b/homeassistant/components/kegtron/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/kegtron/translations/he.json +++ b/homeassistant/components/kegtron/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/kulersky/translations/he.json b/homeassistant/components/kulersky/translations/he.json index d3d68dccc93cc8..4eafc6dc29bd36 100644 --- a/homeassistant/components/kulersky/translations/he.json +++ b/homeassistant/components/kulersky/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/lacrosse_view/translations/he.json b/homeassistant/components/lacrosse_view/translations/he.json index fe6357d0150860..081dd5a372569a 100644 --- a/homeassistant/components/lacrosse_view/translations/he.json +++ b/homeassistant/components/lacrosse_view/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", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", diff --git a/homeassistant/components/lametric/translations/he.json b/homeassistant/components/lametric/translations/he.json index 97c060f6062c7d..0912073e131a3a 100644 --- a/homeassistant/components/lametric/translations/he.json +++ b/homeassistant/components/lametric/translations/he.json @@ -10,6 +10,17 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "manual_entry": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "host": "\u05de\u05d0\u05e8\u05d7" + } + }, + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/hr.json b/homeassistant/components/lametric/translations/hr.json new file mode 100644 index 00000000000000..27a5722e0b4049 --- /dev/null +++ b/homeassistant/components/lametric/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponovna provjera autenti\u010dnosti je uspje\u0161na" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/select.hr.json b/homeassistant/components/lametric/translations/select.hr.json new file mode 100644 index 00000000000000..b4957eae992e85 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.hr.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatski", + "manual": "Ru\u010dno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/he.json b/homeassistant/components/landisgyr_heat_meter/translations/he.json new file mode 100644 index 00000000000000..25d9c78d8cf96d --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/he.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" + } + }, + "user": { + "data": { + "device": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/he.json b/homeassistant/components/led_ble/translations/he.json index 6dc5ae75df8282..3f0c3c6d1f2dc6 100644 --- a/homeassistant/components/led_ble/translations/he.json +++ b/homeassistant/components/led_ble/translations/he.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/lifx/translations/he.json b/homeassistant/components/lifx/translations/he.json index 0ea2e6e551b78c..e40655a5fbd259 100644 --- a/homeassistant/components/lifx/translations/he.json +++ b/homeassistant/components/lifx/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "error": { diff --git a/homeassistant/components/lifx/translations/hr.json b/homeassistant/components/lifx/translations/hr.json new file mode 100644 index 00000000000000..d7692d2d6cfcc3 --- /dev/null +++ b/homeassistant/components/lifx/translations/hr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei", + "single_instance_allowed": "Ve\u0107 konfigurirano. Mogu\u0107a samo jedna konfiguracija." + }, + "step": { + "confirm": { + "description": "\u017delite li postaviti LIFX?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/he.json b/homeassistant/components/lookin/translations/he.json index 3110857a512601..e44e85f614d309 100644 --- a/homeassistant/components/lookin/translations/he.json +++ b/homeassistant/components/lookin/translations/he.json @@ -4,11 +4,11 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/lutron_caseta/translations/he.json b/homeassistant/components/lutron_caseta/translations/he.json index cb742b61b72d03..5bebea072fc86c 100644 --- a/homeassistant/components/lutron_caseta/translations/he.json +++ b/homeassistant/components/lutron_caseta/translations/he.json @@ -19,7 +19,7 @@ "data": { "host": "\u05de\u05d0\u05e8\u05d7" }, - "description": "\u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4- IP \u05e9\u05dc \u05d4\u05de\u05db\u05e9\u05d9\u05e8." + "description": "\u05d9\u05e9 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4-IP \u05e9\u05dc \u05d4\u05d4\u05ea\u05e7\u05df." } } } diff --git a/homeassistant/components/mailgun/translations/hr.json b/homeassistant/components/mailgun/translations/hr.json new file mode 100644 index 00000000000000..90563173adf99a --- /dev/null +++ b/homeassistant/components/mailgun/translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "Jeste li sigurni da \u017eelite postaviti Mailgun?", + "title": "Postavite Mailgun Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/he.json b/homeassistant/components/mazda/translations/he.json index 9856fb9034c2ed..269b5952679d46 100644 --- a/homeassistant/components/mazda/translations/he.json +++ b/homeassistant/components/mazda/translations/he.json @@ -16,7 +16,7 @@ "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d1\u05d4\u05df \u05d0\u05ea\u05d4 \u05de\u05e9\u05ea\u05de\u05e9 \u05db\u05d3\u05d9 \u05dc\u05d4\u05d9\u05db\u05e0\u05e1 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd MyMazda \u05dc\u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd." + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d3\u05d5\u05d0\"\u05dc \u05d5\u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d1\u05d4\u05df \u05d4\u05d9\u05e0\u05da \u05de\u05e9\u05ea\u05de\u05e9 \u05db\u05d3\u05d9 \u05dc\u05d4\u05d9\u05db\u05e0\u05e1 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd MyMazda \u05dc\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e0\u05d9\u05d9\u05d3\u05d9\u05dd." } } } diff --git a/homeassistant/components/meteoclimatic/translations/he.json b/homeassistant/components/meteoclimatic/translations/he.json index db961a2f14c48b..71e6e6b69431be 100644 --- a/homeassistant/components/meteoclimatic/translations/he.json +++ b/homeassistant/components/meteoclimatic/translations/he.json @@ -5,7 +5,7 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { - "not_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "not_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "step": { "user": { diff --git a/homeassistant/components/moat/translations/he.json b/homeassistant/components/moat/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/moat/translations/he.json +++ b/homeassistant/components/moat/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/mqtt/translations/hr.json b/homeassistant/components/mqtt/translations/hr.json index 6ddc827fff921e..c18a994254f5b0 100644 --- a/homeassistant/components/mqtt/translations/hr.json +++ b/homeassistant/components/mqtt/translations/hr.json @@ -1,11 +1,76 @@ { "config": { + "error": { + "bad_certificate": "CA certifikat nije valjan", + "bad_client_cert": "Klijentski certifikat nije valjan, osigurajte da je isporu\u010dena PEM kodirana datoteka", + "bad_client_cert_key": "Klijentski certifikat i privatni klju\u010d nisu valjani par", + "bad_client_key": "Privatni klju\u010d nije valjan, osigurajte da je PEM kodirana datoteka isporu\u010dena bez lozinke", + "bad_discovery_prefix": "Prefiks otkrivanja nije valjan", + "cannot_connect": "Povezivanje nije uspjelo", + "invalid_inclusion": "Klijentski certifikat i privatni klju\u010d moraju biti konfigurirani zajedno" + }, "step": { "broker": { "data": { + "advanced_options": "Napredne opcije", + "broker": "Broker", + "certificate": "Put do datoteke CA certifikata", + "client_cert": "Put do datoteke klijentskog certifikata", + "client_id": "ID klijenta (ostavite prazno za nasumi\u010dno generiran)", + "client_key": "Put do datoteke privatnog klju\u010da", + "discovery": "Omogu\u0107i otkrivanje", + "keepalive": "Vrijeme izme\u0111u slanja poruka keep alive", "password": "Lozinka", "port": "Port", + "protocol": "MQTT protokol", + "set_ca_cert": "Provjera valjanosti brokerskog certifikata", + "set_client_cert": "Kori\u0161tenje klijentskog certifikata", + "tls_insecure": "Zanemari provjeru valjanosti certifikata brokera", "username": "Korisni\u010dko ime" + }, + "description": "Unesite podatke o povezivanju svog MQTT brokera." + }, + "hassio_confirm": { + "data": { + "discovery": "Omogu\u0107i otkrivanje" + }, + "description": "\u017delite li konfigurirati Home Assistant za povezivanje s MQTT brokerom koji pru\u017ea dodatak {addon}?", + "title": "MQTT Broker putem dodatka Home Assistant" + } + } + }, + "issues": { + "deprecated_yaml_broker_settings": { + "title": "Zastarjele MQTT postavke prona\u0111ene u 'configuration.yaml'" + } + }, + "options": { + "error": { + "bad_certificate": "CA certifikat nije valjan", + "bad_client_cert": "Klijentski certifikat nije valjan, osigurajte da je isporu\u010dena PEM kodirana datoteka", + "bad_client_cert_key": "Klijentski certifikat i privatni klju\u010d nisu valjani par", + "bad_client_key": "Privatni klju\u010d nije valjan, osigurajte da je PEM kodirana datoteka isporu\u010dena bez lozinke", + "bad_discovery_prefix": "Prefiks otkrivanja nije valjan", + "invalid_inclusion": "Klijentski certifikat i privatni klju\u010d moraju biti konfigurirani zajedno" + }, + "step": { + "broker": { + "data": { + "advanced_options": "Napredne opcije", + "certificate": "Prijenos datoteke CA certifikata", + "client_cert": "Prijenos datoteke klijentskog certifikata", + "client_id": "ID klijenta (ostavite prazno za nasumi\u010dno generiran)", + "client_key": "Prijenos datoteke privatnog klju\u010da", + "keepalive": "Vrijeme izme\u0111u slanja poruka keep alive", + "protocol": "MQTT protokol", + "set_ca_cert": "Provjera valjanosti certifikata brokera", + "set_client_cert": "Kori\u0161tenje klijentskog certifikata", + "tls_insecure": "Zanemari provjeru valjanosti certifikata brokera" + } + }, + "options": { + "data": { + "discovery_prefix": "Prefiks otkrivanja" } } } diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 08b9c85b110e2f..c6c67cea9c7328 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -73,6 +73,7 @@ "title": "Entitas MQTT {platform} yang dikonfigurasi secara manual membutuhkan perhatian" }, "deprecated_yaml_broker_settings": { + "description": "Pengaturan berikut yang ditemukan di `configuration.yaml` dimigrasikan ke entri konfigurasi MQTT dan sekarang akan menimpa pengaturan di `configuration.yaml`:\n`{deprecated_settings}`\n\nHapus pengaturan ini dari `configuration.yaml` dan mulai ulang Home Assistant untuk memperbaiki masalah ini. Lihat [dokumentasi]({more_info_url}), untuk informasi lebih lanjut.", "title": "Pengaturan MQTT yang usang ditemukan di `configuration.yaml`" } }, @@ -95,8 +96,15 @@ "broker": "Broker", "certificate": "Unggah file sertifikat CA khusus", "client_cert": "Unggah file sertifikat klien", + "client_id": "ID Klien (biarkan kosong agar dibuat secara acak)", + "client_key": "Unggah file kunci pribadi", + "keepalive": "Waktu antara mengirim pesan keep alive", "password": "Kata Sandi", "port": "Port", + "protocol": "Protokol MQTT", + "set_ca_cert": "Validasi sertifikat broker", + "set_client_cert": "Gunakan sertifikat klien", + "tls_insecure": "Abaikan validasi sertifikat broker", "username": "Nama Pengguna" }, "description": "Masukkan informasi koneksi broker MQTT Anda.", @@ -110,6 +118,7 @@ "birth_retain": "Simpan pesan birth", "birth_topic": "Topik pesan birth", "discovery": "Aktifkan penemuan", + "discovery_prefix": "Prefiks penemuan", "will_enable": "Aktifkan pesan 'will'", "will_payload": "Payload pesan will", "will_qos": "QoS pesan will", diff --git a/homeassistant/components/mullvad/translations/he.json b/homeassistant/components/mullvad/translations/he.json index 1551f5e6bb03d9..4b761e279f957e 100644 --- a/homeassistant/components/mullvad/translations/he.json +++ b/homeassistant/components/mullvad/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05d4\u05de\u05db\u05e9\u05d9\u05e8 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { "cannot_connect": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/nest/translations/hr.json b/homeassistant/components/nest/translations/hr.json index d12df4db83b72c..00eb2bf0d16a73 100644 --- a/homeassistant/components/nest/translations/hr.json +++ b/homeassistant/components/nest/translations/hr.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "authorize_url_timeout": "Isteklo je generiranje URL-a za autorizaciju." + }, + "error": { + "unknown": "Neo\u010dekivana gre\u0161ka" + }, "step": { "init": { "data": { "flow_impl": "Pru\u017eatelj usluge" }, + "description": "Odaberite metodu za autorizaciju", "title": "Pru\u017eatelj usluge autentifikacije" }, "link": { diff --git a/homeassistant/components/nibe_heatpump/translations/id.json b/homeassistant/components/nibe_heatpump/translations/id.json index 53e3d2028770a9..3ee3210ee768d1 100644 --- a/homeassistant/components/nibe_heatpump/translations/id.json +++ b/homeassistant/components/nibe_heatpump/translations/id.json @@ -4,7 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "address": "Alamat IP jarak jauh yang ditentukan tidak valid. Alamat harus berupa alamat IPv4.", + "address": "Alamat IP jarak jauh yang ditentukan tidak valid. Alamat harus berupa alamat IP atau nama host yang dapat ditemukan.", "address_in_use": "Port mendengarkan yang dipilih sudah digunakan pada sistem ini.", "model": "Model yang dipilih tampaknya tidak mendukung modbus40", "read": "Kesalahan pada permintaan baca dari pompa. Verifikasi `Port baca jarak jauh` atau `Alamat IP jarak jauh` Anda.", @@ -14,11 +14,18 @@ "step": { "user": { "data": { - "ip_address": "Alamat IP jarak jauh", + "ip_address": "Alamat jarak jauh", "listening_port": "Port mendengarkan lokal", "remote_read_port": "Port baca jarak jauh", "remote_write_port": "Port tulis jarak jauh" - } + }, + "data_description": { + "ip_address": "Alamat unit NibeGW. Perangkat harus dikonfigurasi dengan alamat statis.", + "listening_port": "Port lokal pada sistem ini, tempat unit NibeGW dikonfigurasi untuk mengirim data.", + "remote_read_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan baca.", + "remote_write_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan tulis." + }, + "description": "Sebelum mencoba mengkonfigurasi integrasi, pastikan bahwa:\n- Unit NibeGW terhubung ke pompa pemanas.\n- Aksesori MODBUS40 telah diaktifkan dalam konfigurasi pompa pemanas.\n- Pompa belum masuk ke dalam kondisi alarm tentang ketiadaan aksesori MODBUS40." } } } diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index 727457fac3488a..76a421947d7ef4 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -38,7 +38,7 @@ "data": { "auto": "\u05d7\u05d9\u05e4\u05d5\u05e9 \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9" }, - "description": "\u05d1\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e0\u05d7\u05e4\u05e9 \u05d1\u05e8\u05e9\u05ea \u05e9\u05dc\u05da \u05de\u05db\u05e9\u05d9\u05e8\u05d9 ONVIF \u05d4\u05ea\u05d5\u05de\u05db\u05d9\u05dd \u05d1\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc S.\n\n\u05d9\u05e6\u05e8\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d4\u05d7\u05dc\u05d5 \u05dc\u05d4\u05e9\u05d1\u05d9\u05ea \u05d0\u05ea ONVIF \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05e0\u05d0 \u05d5\u05d3\u05d0 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea ONVIF \u05d6\u05de\u05d9\u05e0\u05d4 \u05d1\u05de\u05e6\u05dc\u05de\u05d4 \u05e9\u05dc\u05da.", + "description": "\u05d1\u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e0\u05d7\u05e4\u05e9 \u05d1\u05e8\u05e9\u05ea \u05e9\u05dc\u05da \u05d4\u05ea\u05e7\u05e0\u05d9 ONVIF \u05d4\u05ea\u05d5\u05de\u05db\u05d9\u05dd \u05d1\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc S.\n\n\u05d9\u05e6\u05e8\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d4\u05d7\u05dc\u05d5 \u05dc\u05d4\u05e9\u05d1\u05d9\u05ea \u05d0\u05ea ONVIF \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05e0\u05d0 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea ONVIF \u05d6\u05de\u05d9\u05e0\u05d4 \u05d1\u05de\u05e6\u05dc\u05de\u05d4 \u05e9\u05dc\u05da.", "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05ea\u05e7\u05df ONVIF" } } diff --git a/homeassistant/components/openexchangerates/translations/he.json b/homeassistant/components/openexchangerates/translations/he.json new file mode 100644 index 00000000000000..34122738a718f6 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/hr.json b/homeassistant/components/openuv/translations/hr.json index 835929d26dfe44..e18b0015d5120c 100644 --- a/homeassistant/components/openuv/translations/hr.json +++ b/homeassistant/components/openuv/translations/hr.json @@ -1,12 +1,17 @@ { "config": { + "error": { + "invalid_api_key": "Neva\u017ee\u0107i API klju\u010d" + }, "step": { "user": { "data": { + "api_key": "API klju\u010d", "elevation": "Elevacija", "latitude": "Zemljopisna \u0161irina", "longitude": "Zemljopisna du\u017eina" - } + }, + "title": "Ispunite svoje podatke" } } } diff --git a/homeassistant/components/oralb/translations/he.json b/homeassistant/components/oralb/translations/he.json index b182a698234a65..e34a0c9d5252be 100644 --- a/homeassistant/components/oralb/translations/he.json +++ b/homeassistant/components/oralb/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" }, "flow_title": "{name}", diff --git a/homeassistant/components/oralb/translations/hr.json b/homeassistant/components/oralb/translations/hr.json new file mode 100644 index 00000000000000..e592ffe7bb5bce --- /dev/null +++ b/homeassistant/components/oralb/translations/hr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "already_in_progress": "Konfiguracije je ve\u0107 u tijeku", + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei", + "not_supported": "Ure\u0111aj nije podr\u017ean" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u017delite li postaviti {name}?" + }, + "user": { + "data": { + "address": "Ure\u0111aj" + }, + "description": "Odaberite ure\u0111aj za postavljanje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/id.json b/homeassistant/components/oralb/translations/id.json new file mode 100644 index 00000000000000..573eb39ed15af7 --- /dev/null +++ b/homeassistant/components/oralb/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Perangkat tidak didukung" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/hr.json b/homeassistant/components/overkiz/translations/hr.json new file mode 100644 index 00000000000000..4dc5421011a1ac --- /dev/null +++ b/homeassistant/components/overkiz/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unsupported_hardware": "Va\u0161 {unsupported_device} hardver nije podr\u017ean ovom integracijom." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/id.json b/homeassistant/components/ovo_energy/translations/id.json index fa072b59236d3f..644dc357d72210 100644 --- a/homeassistant/components/ovo_energy/translations/id.json +++ b/homeassistant/components/ovo_energy/translations/id.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "ID akun OVO (hanya tambahkan jika Anda memiliki beberapa akun)", "password": "Kata Sandi", "username": "Nama Pengguna" }, diff --git a/homeassistant/components/plugwise/translations/select.hr.json b/homeassistant/components/plugwise/translations/select.hr.json new file mode 100644 index 00000000000000..65d546a205d401 --- /dev/null +++ b/homeassistant/components/plugwise/translations/select.hr.json @@ -0,0 +1,17 @@ +{ + "state": { + "plugwise__dhw_mode": { + "auto": "Auto", + "boost": "Poja\u010dano", + "comfort": "Udobnost", + "off": "Isklju\u010deno" + }, + "plugwise__regulation_mode": { + "bleeding_cold": "Jako hladno", + "bleeding_hot": "Jako vru\u0107e", + "cooling": "Hla\u0111enje", + "heating": "Grijanje", + "off": "Isklju\u010deno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/select.id.json b/homeassistant/components/plugwise/translations/select.id.json index 0be50360a0fa4d..3eef4c30aa58e4 100644 --- a/homeassistant/components/plugwise/translations/select.id.json +++ b/homeassistant/components/plugwise/translations/select.id.json @@ -1,5 +1,11 @@ { "state": { + "plugwise__dhw_mode": { + "auto": "Otomatis", + "boost": "Kencang", + "comfort": "Nyaman", + "off": "Mati" + }, "plugwise__regulation_mode": { "bleeding_cold": "Dingin sekali", "bleeding_hot": "Panas sekali", diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json index 0aaba028b7e91c..fe4f5d7b8d296f 100644 --- a/homeassistant/components/ps4/translations/he.json +++ b/homeassistant/components/ps4/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/pushbullet/translations/bg.json b/homeassistant/components/pushbullet/translations/bg.json new file mode 100644 index 00000000000000..11cf3c2b1ed0f3 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "name": "\u0418\u043c\u0435" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Pushbullet \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Pushbullet \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Pushbullet \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/et.json b/homeassistant/components/pushbullet/translations/et.json new file mode 100644 index 00000000000000..85b090c2a4768f --- /dev/null +++ b/homeassistant/components/pushbullet/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Kehtetu API v\u00f5ti" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "name": "Nimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Pushbulleti konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nSelle probleemi lahendamiseks eemalda failist configuration.yaml Pushbullet YAML konfiguratsioon ja taask\u00e4ivita Home Assistant.", + "title": "Pushbulleti YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/he.json b/homeassistant/components/pushbullet/translations/he.json new file mode 100644 index 00000000000000..5f98f90ec8a9c8 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/hr.json b/homeassistant/components/pushbullet/translations/hr.json new file mode 100644 index 00000000000000..01f02cb4496064 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/hr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Servis je ve\u0107 konfiguriran" + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo", + "invalid_api_key": "Neispravan API klju\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API klju\u010d", + "name": "Ime" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "Pushbullet YAML konfiguracija se uklanja" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/id.json b/homeassistant/components/pushbullet/translations/id.json new file mode 100644 index 00000000000000..6a5922193f9eb6 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "name": "Nama" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi Pushbullet lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Pushbullet dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Pushbullet dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/no.json b/homeassistant/components/pushbullet/translations/no.json new file mode 100644 index 00000000000000..347ffdd05a52a8 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "name": "Navn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Pushbullet ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Pushbullet YAML-konfigurasjonen fra filen configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Pushbullet YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/pl.json b/homeassistant/components/pushbullet/translations/pl.json new file mode 100644 index 00000000000000..eedc36d33e4c06 --- /dev/null +++ b/homeassistant/components/pushbullet/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "name": "Nazwa" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Pushbullet przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Pushbullet zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/zh-Hant.json b/homeassistant/components/pushbullet/translations/zh-Hant.json new file mode 100644 index 00000000000000..2c641160218d8f --- /dev/null +++ b/homeassistant/components/pushbullet/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Pushbullet \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushbullet YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Pushbullet YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/he.json b/homeassistant/components/pushover/translations/he.json index 9cdb8c5afcd913..954ccefdde0878 100644 --- a/homeassistant/components/pushover/translations/he.json +++ b/homeassistant/components/pushover/translations/he.json @@ -1,6 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/pushover/translations/id.json b/homeassistant/components/pushover/translations/id.json index 347f4ef5d3e3f1..b7aa1d9fd33682 100644 --- a/homeassistant/components/pushover/translations/id.json +++ b/homeassistant/components/pushover/translations/id.json @@ -29,6 +29,10 @@ "deprecated_yaml": { "description": "Proses konfigurasi Integrasi Pushover lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", "title": "Konfigurasi YAML Integrasi Pushover dalam proses penghapusan" + }, + "removed_yaml": { + "description": "Proses konfigurasi Integrasi Pushover lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Pushover telah dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/he.json b/homeassistant/components/qingping/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/qingping/translations/he.json +++ b/homeassistant/components/qingping/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/rainmachine/translations/id.json b/homeassistant/components/rainmachine/translations/id.json index 8223fb4a792d7a..3c1434b7b4b2d0 100644 --- a/homeassistant/components/rainmachine/translations/id.json +++ b/homeassistant/components/rainmachine/translations/id.json @@ -35,6 +35,7 @@ "step": { "init": { "data": { + "use_app_run_times": "Gunakan waktu berjalan zona dari aplikasi RainMachine", "zone_run_time": "Waktu berjalan zona default (dalam detik)" }, "title": "Konfigurasikan RainMachine" diff --git a/homeassistant/components/scrape/translations/id.json b/homeassistant/components/scrape/translations/id.json index e7761f73a1f509..0ee76592dfe9a2 100644 --- a/homeassistant/components/scrape/translations/id.json +++ b/homeassistant/components/scrape/translations/id.json @@ -36,6 +36,12 @@ } } }, + "issues": { + "moved_yaml": { + "description": "Konfigurasi Integrasi Scrape menggunakan YAML telah dipindahkan ke kunci integrasi.\n\nKonfigurasi YAML Anda yang ada saat ini akan berfungsi hingga 2 versi berikutnya.\n\nMigrasikan konfigurasi YAML Anda ke kunci integrasi sesuai dengan dokumentasi.", + "title": "Konfigurasi YAML Integrasi Scrape telah dihapus" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index fc33b6e7698358..64558ebb8522c4 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -17,7 +17,7 @@ "api_key": "Kunci API" }, "data_description": { - "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API baru." + "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API" } }, "user": { @@ -25,7 +25,7 @@ "api_key": "Kunci API" }, "data_description": { - "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API." + "api_key": "Ikuti petunjuk dalam dokumentasi untuk mendapatkan kunci API" } } } diff --git a/homeassistant/components/sensibo/translations/sensor.hr.json b/homeassistant/components/sensibo/translations/sensor.hr.json new file mode 100644 index 00000000000000..26f61fd8d34e61 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.hr.json @@ -0,0 +1,9 @@ +{ + "state": { + "sensibo__smart_type": { + "feelslike": "Osje\u0107aj", + "humidity": "Vla\u017enost", + "temperature": "Temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.id.json b/homeassistant/components/sensibo/translations/sensor.id.json index 54a0554ce41ebf..29fb97771171ef 100644 --- a/homeassistant/components/sensibo/translations/sensor.id.json +++ b/homeassistant/components/sensibo/translations/sensor.id.json @@ -3,6 +3,11 @@ "sensibo__sensitivity": { "n": "Normal", "s": "Sensitif" + }, + "sensibo__smart_type": { + "feelslike": "Terasa seperti", + "humidity": "Kelembaban", + "temperature": "Suhu" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index e9e2340fed0d2b..7a78276754789b 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -32,6 +32,7 @@ "is_volatile_organic_compounds": "Tingkat konsentrasi senyawa organik volatil {entity_name} saat ini", "is_voltage": "Tegangan {entity_name} saat ini", "is_volume": "Volume {entity_name} saat ini", + "is_water": "Air pada {entity_name} saat ini", "is_weight": "Berat {entity_name} saat ini" }, "trigger_type": { @@ -66,6 +67,7 @@ "volatile_organic_compounds": "Perubahan konsentrasi senyawa organik volatil {entity_name}", "voltage": "Perubahan tegangan {entity_name}", "volume": "Perubahan volume {entity_name}", + "water": "Perubahan air {entity_name}", "weight": "Perubahan berat {entity_name}" } }, diff --git a/homeassistant/components/sensorpro/translations/he.json b/homeassistant/components/sensorpro/translations/he.json index b182a698234a65..e34a0c9d5252be 100644 --- a/homeassistant/components/sensorpro/translations/he.json +++ b/homeassistant/components/sensorpro/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" }, "flow_title": "{name}", diff --git a/homeassistant/components/sensorpush/translations/he.json b/homeassistant/components/sensorpush/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/sensorpush/translations/he.json +++ b/homeassistant/components/sensorpush/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/simplisafe/translations/hr.json b/homeassistant/components/simplisafe/translations/hr.json new file mode 100644 index 00000000000000..addfc0cbe8102e --- /dev/null +++ b/homeassistant/components/simplisafe/translations/hr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Lozinka", + "username": "Korisni\u010dko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/he.json b/homeassistant/components/skybell/translations/he.json index 0e3ced77bc3477..f85a3c23279c55 100644 --- a/homeassistant/components/skybell/translations/he.json +++ b/homeassistant/components/skybell/translations/he.json @@ -13,7 +13,8 @@ "reauth_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - } + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, "user": { "data": { diff --git a/homeassistant/components/smhi/translations/hr.json b/homeassistant/components/smhi/translations/hr.json new file mode 100644 index 00000000000000..0ceab312f1e520 --- /dev/null +++ b/homeassistant/components/smhi/translations/hr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "wrong_location": "Lokacija samo \u0160vedska" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina" + }, + "title": "Lokacija u \u0160vedskoj" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/he.json b/homeassistant/components/snooz/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/snooz/translations/he.json +++ b/homeassistant/components/snooz/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/snooz/translations/hr.json b/homeassistant/components/snooz/translations/hr.json new file mode 100644 index 00000000000000..ff7217c53525bf --- /dev/null +++ b/homeassistant/components/snooz/translations/hr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "already_in_progress": "Konfiguracije je ve\u0107 u tijeku", + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u017delite li postaviti {name}?" + }, + "user": { + "data": { + "address": "Ure\u0111aj" + }, + "description": "Odaberite ure\u0111aj za postavljanje" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/he.json b/homeassistant/components/songpal/translations/he.json index a8c8d1d0294ba8..a90c9b9085894f 100644 --- a/homeassistant/components/songpal/translations/he.json +++ b/homeassistant/components/songpal/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05de\u05db\u05e9\u05d9\u05e8 \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/sonos/translations/he.json b/homeassistant/components/sonos/translations/he.json index 64824b942ec605..5e29646acfb529 100644 --- a/homeassistant/components/sonos/translations/he.json +++ b/homeassistant/components/sonos/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "not_sonos_device": "\u05d4\u05ea\u05e7\u05df \u05e9\u05d4\u05ea\u05d2\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05d4\u05ea\u05e7\u05df Sonos", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json index cabbb3a62240ee..f267a63ba1a565 100644 --- a/homeassistant/components/soundtouch/translations/id.json +++ b/homeassistant/components/soundtouch/translations/id.json @@ -13,7 +13,7 @@ } }, "zeroconf_confirm": { - "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", + "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", "title": "Konfirmasi penambahan perangkat Bose SoundTouch" } } diff --git a/homeassistant/components/statistics/translations/he.json b/homeassistant/components/statistics/translations/he.json index fca507bcc19c4d..b5cb3b2475402f 100644 --- a/homeassistant/components/statistics/translations/he.json +++ b/homeassistant/components/statistics/translations/he.json @@ -5,7 +5,8 @@ "title": "\u05d7\u05d5\u05d1\u05d4 'state_characteristic' \u05e9\u05d4\u05d5\u05e0\u05d7\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4" }, "deprecation_warning_size": { - "description": "\u05e4\u05e8\u05de\u05d8\u05e8 \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 `sampling_size` \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc \u05dc\u05e2\u05e8\u05da 20 \u05e2\u05d3 \u05db\u05d4, \u05d0\u05e9\u05e8 \u05d9\u05e9\u05ea\u05e0\u05d4.\n\n\u05d0\u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df `{entity}` \u05d5\u05d4\u05d5\u05e1\u05e3 \u05d2\u05d1\u05d5\u05dc\u05d5\u05ea \u05de\u05ea\u05d0\u05d9\u05de\u05d9\u05dd, \u05dc\u05d3\u05d5\u05d2\u05de\u05d4, `sampling_size: 20` \u05db\u05d3\u05d9 \u05dc\u05e9\u05de\u05d5\u05e8 \u05e2\u05dc \u05d4\u05d4\u05ea\u05e0\u05d4\u05d2\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea. \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05ea\u05d4\u05e4\u05d5\u05da \u05dc\u05d2\u05de\u05d9\u05e9\u05d4 \u05d9\u05d5\u05ea\u05e8 \u05e2\u05dd \u05d2\u05e8\u05e1\u05d4 2022.12.0 \u05d5\u05ea\u05e7\u05d1\u05dc `sampling_size` \u05d0\u05d5 `max_age`, \u05d0\u05d5 \u05d0\u05ea \u05e9\u05ea\u05d9 \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d4\u05d1\u05e7\u05e9\u05d4 \u05e9\u05dc\u05e2\u05d9\u05dc \u05de\u05db\u05d9\u05e0\u05d4 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05d6\u05d4 \u05e9\u05d0\u05d7\u05e8\u05ea \u05e0\u05e9\u05d1\u05e8.\n\n\u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05dc\u05e4\u05e8\u05d8\u05d9\u05dd \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd: https://www.home-assistant.io/integrations/statistics/" + "description": "\u05e4\u05e8\u05de\u05d8\u05e8 \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 `sampling_size` \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05d1\u05e8\u05d9\u05e8\u05ea \u05d4\u05de\u05d7\u05d3\u05dc \u05dc\u05e2\u05e8\u05da 20 \u05e2\u05d3 \u05db\u05d4, \u05d0\u05e9\u05e8 \u05d9\u05e9\u05ea\u05e0\u05d4.\n\n\u05d0\u05e0\u05d0 \u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df `{entity}` \u05d5\u05d4\u05d5\u05e1\u05e3 \u05d2\u05d1\u05d5\u05dc\u05d5\u05ea \u05de\u05ea\u05d0\u05d9\u05de\u05d9\u05dd, \u05dc\u05d3\u05d5\u05d2\u05de\u05d4, `sampling_size: 20` \u05db\u05d3\u05d9 \u05dc\u05e9\u05de\u05d5\u05e8 \u05e2\u05dc \u05d4\u05d4\u05ea\u05e0\u05d4\u05d2\u05d5\u05ea \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea. \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05ea\u05d4\u05e4\u05d5\u05da \u05dc\u05d2\u05de\u05d9\u05e9\u05d4 \u05d9\u05d5\u05ea\u05e8 \u05e2\u05dd \u05d2\u05e8\u05e1\u05d4 2022.12.0 \u05d5\u05ea\u05e7\u05d1\u05dc `sampling_size` \u05d0\u05d5 `max_age`, \u05d0\u05d5 \u05d0\u05ea \u05e9\u05ea\u05d9 \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d4\u05d1\u05e7\u05e9\u05d4 \u05e9\u05dc\u05e2\u05d9\u05dc \u05de\u05db\u05d9\u05e0\u05d4 \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05d6\u05d4 \u05e9\u05d0\u05d7\u05e8\u05ea \u05e0\u05e9\u05d1\u05e8.\n\n\u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05dc\u05e4\u05e8\u05d8\u05d9\u05dd \u05e0\u05d5\u05e1\u05e4\u05d9\u05dd: https://www.home-assistant.io/integrations/statistics/", + "title": "'sampling_size' \u05de\u05e8\u05d5\u05de\u05d6\u05ea \u05e9\u05d4\u05d5\u05e0\u05d7\u05d4 \u05e2\u05d1\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05ea" } } } \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/id.json b/homeassistant/components/statistics/translations/id.json new file mode 100644 index 00000000000000..13ac93d21ddff1 --- /dev/null +++ b/homeassistant/components/statistics/translations/id.json @@ -0,0 +1,12 @@ +{ + "issues": { + "deprecation_warning_characteristic": { + "description": "Parameter konfigurasi `state_characteristic` dari integrasi statistik akan menjadi wajib.\n\nTambahkan `state_characteristic: {characteristic}` ke dalam konfigurasi sensor `{entity}` untuk menjaga perilaku saat ini.\n\nBaca dokumentasi integrasi Statistik untuk detail lebih lanjut: https://www.home-assistant.io/integrations/statistics/", + "title": "Parameter wajib 'state_characteristic' diasumsikan untuk entitas Statistik" + }, + "deprecation_warning_size": { + "description": "Parameter konfigurasi `sampling_size` dari integrasi Statistik yang bernilai default 20 sejauh ini, akan berubah.\n\nPeriksa konfigurasi untuk sensor `{entity}` dan tambahkan batas-batas yang sesuai, misalnya, `sampling_size: 20` untuk menjaga perilaku saat ini. Konfigurasi integrasi Statistik akan menjadi lebih fleksibel mulai versi 2022.12.0 dan menerima baik parameter `sampling_size` atau `max_age`, atau keduanya. Permintaan di atas akan mempersiapkan konfigurasi Anda untuk menghadapi perubahan besar ini.\n\nBaca dokumentasi integrasi Statistik untuk detail lebih lanjut: https://www.home-assistant.io/integrations/statistics/", + "title": "Parameter implisit 'sampling_size' diasumsikan untuk entitas Statistik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/bg.json b/homeassistant/components/steam_online/translations/bg.json index 8d946452ca01e1..5cfc98b4803a93 100644 --- a/homeassistant/components/steam_online/translations/bg.json +++ b/homeassistant/components/steam_online/translations/bg.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_account": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d ID \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442\u0430", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/steamist/translations/he.json b/homeassistant/components/steamist/translations/he.json index 7b8528476e1edf..405f691d4b3797 100644 --- a/homeassistant/components/steamist/translations/he.json +++ b/homeassistant/components/steamist/translations/he.json @@ -4,7 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/switcher_kis/translations/he.json b/homeassistant/components/switcher_kis/translations/he.json index d3d68dccc93cc8..4eafc6dc29bd36 100644 --- a/homeassistant/components/switcher_kis/translations/he.json +++ b/homeassistant/components/switcher_kis/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/tautulli/translations/hr.json b/homeassistant/components/tautulli/translations/hr.json new file mode 100644 index 00000000000000..b4c376c38552cb --- /dev/null +++ b/homeassistant/components/tautulli/translations/hr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API klju\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/hr.json b/homeassistant/components/tellduslive/translations/hr.json new file mode 100644 index 00000000000000..4b26b20a8ebd64 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/hr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dekivana gre\u0161ka" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/he.json b/homeassistant/components/thermobeacon/translations/he.json index b182a698234a65..e34a0c9d5252be 100644 --- a/homeassistant/components/thermobeacon/translations/he.json +++ b/homeassistant/components/thermobeacon/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" }, "flow_title": "{name}", diff --git a/homeassistant/components/thermopro/translations/he.json b/homeassistant/components/thermopro/translations/he.json index 47308062d0d426..26219169d120a4 100644 --- a/homeassistant/components/thermopro/translations/he.json +++ b/homeassistant/components/thermopro/translations/he.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/tilt_ble/translations/he.json b/homeassistant/components/tilt_ble/translations/he.json index de780eb221ab27..26219169d120a4 100644 --- a/homeassistant/components/tilt_ble/translations/he.json +++ b/homeassistant/components/tilt_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index fc44b0d7ae790f..8a4dcfb5134acc 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/tradfri/translations/hr.json b/homeassistant/components/tradfri/translations/hr.json index bb242ca60f0890..32fb3fd7fb45b7 100644 --- a/homeassistant/components/tradfri/translations/hr.json +++ b/homeassistant/components/tradfri/translations/hr.json @@ -1,13 +1,20 @@ { "config": { "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", "already_in_progress": "Konfiguracija premosnice je ve\u0107 u tijeku." }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + }, "step": { "auth": { "data": { - "host": "Host" - } + "host": "Host", + "security_code": "Sigurnosni kod" + }, + "description": "Sigurnosni k\u00f4d mo\u017eete prona\u0107i na pole\u0111ini pristupnika.", + "title": "Unesite sigurnosni kod" } } } diff --git a/homeassistant/components/transmission/translations/id.json b/homeassistant/components/transmission/translations/id.json index 7b5fa3a703f914..98a5e918740c28 100644 --- a/homeassistant/components/transmission/translations/id.json +++ b/homeassistant/components/transmission/translations/id.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini dan ganti kunci name dengan kunci entry_id.", + "title": "Kunci name dalam layanan Transmission sedang dihapus" + } + } + }, + "title": "Kunci name dalam layanan Transmission sedang dihapus" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/twilio/translations/hr.json b/homeassistant/components/twilio/translations/hr.json new file mode 100644 index 00000000000000..5307b9f4eb06c1 --- /dev/null +++ b/homeassistant/components/twilio/translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u017delite li zapo\u010deti s postavljanjem?", + "title": "Postavite Twilio Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/hr.json b/homeassistant/components/unifi/translations/hr.json index 94a064f34b4ff1..47ba1743f713f6 100644 --- a/homeassistant/components/unifi/translations/hr.json +++ b/homeassistant/components/unifi/translations/hr.json @@ -1,13 +1,19 @@ { "config": { + "error": { + "service_unavailable": "Povezivanje nije uspjelo" + }, "step": { "user": { "data": { "host": "Host", "password": "Lozinka", "port": "Port", - "username": "Korisni\u010dko ime" - } + "site": "ID lokacije", + "username": "Korisni\u010dko ime", + "verify_ssl": "Provjerite SSL certifikat" + }, + "title": "Postavite UniFi mre\u017eu" } } } diff --git a/homeassistant/components/upcloud/translations/id.json b/homeassistant/components/upcloud/translations/id.json index 4ff6a8c7d92c23..d51e6a2e712ad0 100644 --- a/homeassistant/components/upcloud/translations/id.json +++ b/homeassistant/components/upcloud/translations/id.json @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "scan_interval": "Interval pembaruan (dalam detik, minimal 30)" + "scan_interval": "Interval pembaruan dalam detik, minimal 30" } } } diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json index a8501a7de41adc..36640a492a99d0 100644 --- a/homeassistant/components/upnp/translations/he.json +++ b/homeassistant/components/upnp/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/upnp/translations/hr.json b/homeassistant/components/upnp/translations/hr.json index 941f72f2e7da78..785e860e69b826 100644 --- a/homeassistant/components/upnp/translations/hr.json +++ b/homeassistant/components/upnp/translations/hr.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "no_devices_found": "Nijedan ure\u0111aj nije prona\u0111en na mre\u017ei" + }, "error": { "few": "Nekoliko", "one": "Jedan", "other": "Ostalo" + }, + "step": { + "init": { + "few": "Nekoliko", + "one": "Jedan", + "other": "Ostalo" + } } } } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/hr.json b/homeassistant/components/utility_meter/translations/hr.json new file mode 100644 index 00000000000000..d4e640e4069eaf --- /dev/null +++ b/homeassistant/components/utility_meter/translations/hr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/id.json b/homeassistant/components/utility_meter/translations/id.json index d2f9bab72c5f72..4375e6f9764383 100644 --- a/homeassistant/components/utility_meter/translations/id.json +++ b/homeassistant/components/utility_meter/translations/id.json @@ -26,7 +26,7 @@ "step": { "init": { "data": { - "source": "Sensor Input" + "source": "Sensor input" } } } diff --git a/homeassistant/components/volvooncall/translations/he.json b/homeassistant/components/volvooncall/translations/he.json index 6f2cdbf82e128d..ad8caad16ae83c 100644 --- a/homeassistant/components/volvooncall/translations/he.json +++ b/homeassistant/components/volvooncall/translations/he.json @@ -1,9 +1,11 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { diff --git a/homeassistant/components/volvooncall/translations/id.json b/homeassistant/components/volvooncall/translations/id.json index d4b60911401136..f1090bd250912a 100644 --- a/homeassistant/components/volvooncall/translations/id.json +++ b/homeassistant/components/volvooncall/translations/id.json @@ -23,7 +23,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Proses konfigurasi platform Volvo On Call lewat YAML dalam proses penghapusan di versi mendatang Home Assistant.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "description": "Proses konfigurasi platform Volvo On Call lewat YAML dalam proses penghapusan di versi mendatang Home Assistant.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", "title": "Konfigurasi YAML Volvo On Call dalam proses penghapusan" } } diff --git a/homeassistant/components/wemo/translations/he.json b/homeassistant/components/wemo/translations/he.json index 380dbc5d7fcdc7..032c9c9fa17f7a 100644 --- a/homeassistant/components/wemo/translations/he.json +++ b/homeassistant/components/wemo/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." } } diff --git a/homeassistant/components/wiz/translations/he.json b/homeassistant/components/wiz/translations/he.json index 81b954067f78fd..44f40330fcd41f 100644 --- a/homeassistant/components/wiz/translations/he.json +++ b/homeassistant/components/wiz/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/wolflink/translations/sensor.hr.json b/homeassistant/components/wolflink/translations/sensor.hr.json new file mode 100644 index 00000000000000..98aa80601c8755 --- /dev/null +++ b/homeassistant/components/wolflink/translations/sensor.hr.json @@ -0,0 +1,7 @@ +{ + "state": { + "wolflink__state": { + "permanent": "Trajno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/he.json b/homeassistant/components/xiaomi_ble/translations/he.json index b90a366130ab0c..0df85dd1fe5d4a 100644 --- a/homeassistant/components/xiaomi_ble/translations/he.json +++ b/homeassistant/components/xiaomi_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "flow_title": "{name}", diff --git a/homeassistant/components/xiaomi_miio/translations/hr.json b/homeassistant/components/xiaomi_miio/translations/hr.json new file mode 100644 index 00000000000000..be104f26c7779c --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dekivana gre\u0161ka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json index 8d1d70642aaaec..f3dbc9877dd3f1 100644 --- a/homeassistant/components/xiaomi_miio/translations/id.json +++ b/homeassistant/components/xiaomi_miio/translations/id.json @@ -5,7 +5,8 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "incomplete_info": "Informasi tidak lengkap untuk menyiapkan perangkat, tidak ada host atau token yang disediakan.", "not_xiaomi_miio": "Perangkat (masih) tidak didukung oleh Xiaomi Miio.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", @@ -36,7 +37,7 @@ "host": "Alamat IP", "token": "Token API" }, - "description": "Anda akan membutuhkan Token API 32 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Perhatikan bahwa Token API ini berbeda dengan kunci yang digunakan untuk integrasi Xiaomi Aqara." + "description": "Anda akan membutuhkan Token API 32 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Perhatikan bahwa Token API ini berbeda dengan kunci yang digunakan untuk integrasi Xiaomi Aqara." }, "reauth_confirm": { "description": "Integrasi Xiaomi Miio perlu mengautentikasi ulang akun Anda untuk memperbarui token atau menambahkan kredensial cloud yang hilang.", diff --git a/homeassistant/components/yalexs_ble/translations/he.json b/homeassistant/components/yalexs_ble/translations/he.json index a447b36c3ec608..5ca18ddcb963f4 100644 --- a/homeassistant/components/yalexs_ble/translations/he.json +++ b/homeassistant/components/yalexs_ble/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", diff --git a/homeassistant/components/yeelight/translations/he.json b/homeassistant/components/yeelight/translations/he.json index 94e0e87d87a7ee..bb11e283db3ddd 100644 --- a/homeassistant/components/yeelight/translations/he.json +++ b/homeassistant/components/yeelight/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" diff --git a/homeassistant/components/zamg/translations/hr.json b/homeassistant/components/zamg/translations/hr.json new file mode 100644 index 00000000000000..bc981183e94d77 --- /dev/null +++ b/homeassistant/components/zamg/translations/hr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ure\u0111aj je ve\u0107 konfiguriran", + "cannot_connect": "Povezivanje nije uspjelo" + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/id.json b/homeassistant/components/zamg/translations/id.json new file mode 100644 index 00000000000000..4330ae8c3de696 --- /dev/null +++ b/homeassistant/components/zamg/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "ID Stasiun (Default ke stasiun terdekat)" + }, + "description": "Siapkan ZAMG untuk diintegrasikan dengan Home Assistant." + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi ZAMG lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi ZAMG dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML ZAMG dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/he.json b/homeassistant/components/zerproc/translations/he.json index 459053197a62b7..c6da7f61442308 100644 --- a/homeassistant/components/zerproc/translations/he.json +++ b/homeassistant/components/zerproc/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05db\u05d1\u05e8 \u05d4\u05d5\u05d2\u05d3\u05e8. \u05e8\u05e7 \u05d4\u05d2\u05d3\u05e8\u05d4 \u05d9\u05d7\u05d9\u05d3\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, "step": { diff --git a/homeassistant/components/zha/translations/he.json b/homeassistant/components/zha/translations/he.json index f48b30bd826c3a..93c614699f2021 100644 --- a/homeassistant/components/zha/translations/he.json +++ b/homeassistant/components/zha/translations/he.json @@ -57,6 +57,9 @@ "abort": { "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, "flow_title": "{name}", "step": { "init": { diff --git a/homeassistant/components/zha/translations/hr.json b/homeassistant/components/zha/translations/hr.json new file mode 100644 index 00000000000000..2098f53ca2ba45 --- /dev/null +++ b/homeassistant/components/zha/translations/hr.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ve\u0107 konfigurirano. Mogu\u0107a samo jedna konfiguracija." + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + }, + "step": { + "user": { + "title": "ZHA" + } + } + }, + "options": { + "abort": { + "single_instance_allowed": "Ve\u0107 konfigurirano. Mogu\u0107a samo jedna konfiguracija." + }, + "error": { + "cannot_connect": "Povezivanje nije uspjelo" + }, + "step": { + "intent_migrate": { + "title": "Migracija na novi radio" + }, + "prompt_migrate_or_reconfigure": { + "description": "Migrirate li na novi radio ili rekonfigurirate trenutni radio?", + "menu_options": { + "intent_migrate": "Migracija na novi radio", + "intent_reconfigure": "Ponovno konfiguriranje trenutnog radija" + }, + "title": "Migracija ili ponovna konfiguracija" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index ab496b3be53248..10ad82bc98041f 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -61,7 +61,7 @@ "data": { "overwrite_coordinator_ieee": "Ganti alamat radio IEEE secara permanen" }, - "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", + "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", "title": "Timpa Alamat IEEE Radio" }, "pick_radio": { @@ -207,7 +207,7 @@ "title": "Pilih Port Serial" }, "init": { - "description": "ZHA akan dihentikan. Ingin melanjutkan?", + "description": "ZHA akan dihentikan. Ingin melanjutkan?", "title": "Konfigurasi Ulang ZHA" }, "instruct_unplug": { @@ -215,7 +215,7 @@ "title": "Cabut radio lama Anda" }, "intent_migrate": { - "description": "Radio lama Anda akan disetel ulang ke setelan pabrikan. Jika Anda menggunakan adaptor gabungan Z-Wave dan Zigbee seperti HUSBZB-1, ini hanya akan mengatur ulang bagian Zigbee.\n\nApakah Anda ingin melanjutkan?", + "description": "Radio lama Anda akan disetel ulang ke setelan pabrikan. Jika Anda menggunakan adaptor gabungan Z-Wave dan Zigbee seperti HUSBZB-1, ini hanya akan mengatur ulang bagian Zigbee.\n\nApakah Anda ingin melanjutkan?", "title": "Migrasikan ke radio baru" }, "manual_pick_radio_type": { @@ -238,7 +238,7 @@ "data": { "overwrite_coordinator_ieee": "Ganti alamat radio IEEE secara permanen" }, - "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", + "description": "Cadangan Anda memiliki alamat IEEE yang berbeda dari radio Anda. Agar jaringan berfungsi dengan baik, alamat IEEE radio Anda juga harus diubah.\n\nOperasi ini bersifat permanen.", "title": "Timpa Alamat IEEE Radio" }, "prompt_migrate_or_reconfigure": { From 93072d8ac5fa34e57eade45ca227ad9800890da8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Nov 2022 06:34:23 +0100 Subject: [PATCH 219/394] Bump dbus-fast to 1.67.0 (#81517) Bump dbus to 1.67.0 The bleak BlueZ clients use the negotiate_unix_fd path which was not optimized but is now. (The scanners already used a fast path) changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.64.0...v1.67.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index bcb7ce9b15659c..49731df2f9bcdf 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.64.0" + "dbus-fast==1.67.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 160f313d883623..ae7e535dbc0a04 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.3 -dbus-fast==1.64.0 +dbus-fast==1.67.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 225d00966846d5..5a866deb8a6200 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,7 +546,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.64.0 +dbus-fast==1.67.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a7f2351050f03..c6e084742173af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.64.0 +dbus-fast==1.67.0 # homeassistant.components.debugpy debugpy==1.6.3 From 1a5eeb2db11db68d5da59830503b6f3f823ec5e5 Mon Sep 17 00:00:00 2001 From: Nyro Date: Fri, 4 Nov 2022 10:21:30 +0100 Subject: [PATCH 220/394] Add Overkiz AtlanticPassAPCHeatingAndCoolingZone (#78659) * Add Overkiz AtlanticPassAPCHeatingAndCoolingZone * Fix commands instanciations to be simpler * Update AtlanticPassAPCHeatingAndCoolingZone to show temperature and fix HA threads * Simplify async_execute_commands in order to receive simpler list * Fix get and set temperature in derogation or auto mode * Remove hvac_action from AtlanticPassAPCHeatingAndCoolingZone * Remove unused lines * Update async_execute_commands to work like async_execute_command Implement cancel for multiple commands * Improve to use preset_home for internal scheduling * Remove async_execute_commands * Improvement for AtlanticPassAPCHeatingAndCoolingZone * Update homeassistant/components/overkiz/climate_entities/__init__.py Co-authored-by: Quentame * Update homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py Co-authored-by: Quentame * Update homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py Co-authored-by: Quentame * Update homeassistant/components/overkiz/const.py Co-authored-by: Quentame Co-authored-by: Quentame --- .../overkiz/climate_entities/__init__.py | 4 + ...antic_pass_apc_heating_and_cooling_zone.py | 203 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + homeassistant/components/overkiz/entity.py | 5 +- 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 32fae234be128a..359f7629a7b11d 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -4,6 +4,9 @@ from .atlantic_electrical_heater import AtlanticElectricalHeater from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation +from .atlantic_pass_apc_heating_and_cooling_zone import ( + AtlanticPassAPCHeatingAndCoolingZone, +) from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .somfy_thermostat import SomfyThermostat @@ -11,6 +14,7 @@ UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: AtlanticHeatRecoveryVentilation, + UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: AtlanticPassAPCHeatingAndCoolingZone, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, } diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py new file mode 100644 index 00000000000000..efdbf94d9f75fe --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_and_cooling_zone.py @@ -0,0 +1,203 @@ +"""Support for Atlantic Pass APC Heating And Cooling Zone Control.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + PRESET_AWAY, + PRESET_COMFORT, + PRESET_ECO, + PRESET_HOME, + PRESET_SLEEP, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +OVERKIZ_TO_HVAC_MODE: dict[str, str] = { + OverkizCommandParam.AUTO: HVACMode.AUTO, + OverkizCommandParam.ECO: HVACMode.AUTO, + OverkizCommandParam.MANU: HVACMode.HEAT, + OverkizCommandParam.HEATING: HVACMode.HEAT, + OverkizCommandParam.STOP: HVACMode.OFF, + OverkizCommandParam.INTERNAL_SCHEDULING: HVACMode.AUTO, + OverkizCommandParam.COMFORT: HVACMode.HEAT, +} + +HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()} + +OVERKIZ_TO_PRESET_MODES: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_ECO, + OverkizCommandParam.STOP: PRESET_ECO, + OverkizCommandParam.MANU: PRESET_COMFORT, + OverkizCommandParam.COMFORT: PRESET_COMFORT, + OverkizCommandParam.ABSENCE: PRESET_AWAY, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.INTERNAL_SCHEDULING: PRESET_HOME, +} + +PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()} + +OVERKIZ_TO_PROFILE_MODES: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_SLEEP, + OverkizCommandParam.STOP: PRESET_SLEEP, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.ABSENCE: PRESET_AWAY, + OverkizCommandParam.MANU: PRESET_COMFORT, + OverkizCommandParam.DEROGATION: PRESET_COMFORT, + OverkizCommandParam.COMFORT: PRESET_COMFORT, +} + +OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: dict[str, str] = { + OverkizCommandParam.ECO: OverkizState.CORE_ECO_HEATING_TARGET_TEMPERATURE, + OverkizCommandParam.COMFORT: OverkizState.CORE_COMFORT_HEATING_TARGET_TEMPERATURE, + OverkizCommandParam.DEROGATION: OverkizState.CORE_DEROGATED_TARGET_TEMPERATURE, +} + + +class AtlanticPassAPCHeatingAndCoolingZone(OverkizEntity, ClimateEntity): + """Representation of Atlantic Pass APC Heating And Cooling Zone Control.""" + + _attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE + ) + _attr_temperature_unit = TEMP_CELSIUS + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + + # Temperature sensor use the same base_device_url and use the n+1 index + self.temperature_device = self.executor.linked_device( + int(self.index_device_url) + 1 + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return cast(float, temperature.value) + + return None + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return OVERKIZ_TO_HVAC_MODE[ + cast(str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE)) + ] + + @property + def current_heating_profile(self) -> str: + """Return current heating profile.""" + return cast( + str, + self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_PROFILE), + ) + + async def async_set_heating_mode(self, mode: str) -> None: + """Set new heating mode and refresh states.""" + await self.executor.async_execute_command( + OverkizCommand.SET_PASS_APC_HEATING_MODE, mode + ) + + if self.current_heating_profile == OverkizCommandParam.DEROGATION: + # If current mode is in derogation, disable it + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION_ON_OFF_STATE, OverkizCommandParam.OFF + ) + + # We also needs to execute these 2 commands to make it work correctly + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_MODE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_PROFILE + ) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.async_set_heating_mode(HVAC_MODE_TO_OVERKIZ[hvac_mode]) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode]) + + @property + def preset_mode(self) -> str: + """Return the current preset mode, e.g., home, away, temp.""" + heating_mode = cast( + str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE) + ) + + if heating_mode == OverkizCommandParam.INTERNAL_SCHEDULING: + # In Internal scheduling, it could be comfort or eco + return OVERKIZ_TO_PROFILE_MODES[ + cast( + str, + self.executor.select_state( + OverkizState.IO_PASS_APC_HEATING_PROFILE + ), + ) + ] + + return OVERKIZ_TO_PRESET_MODES[heating_mode] + + @property + def target_temperature(self) -> float: + """Return hvac target temperature.""" + current_heating_profile = self.current_heating_profile + if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: + return cast( + float, + self.executor.select_state( + OVERKIZ_TEMPERATURE_STATE_BY_PROFILE[current_heating_profile] + ), + ) + return cast( + float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE) + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + + if self.hvac_mode == HVACMode.AUTO: + await self.executor.async_execute_command( + OverkizCommand.SET_COMFORT_HEATING_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_COMFORT_HEATING_TARGET_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + else: + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATED_TARGET_TEMPERATURE, + temperature, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION_ON_OFF_STATE, + OverkizCommandParam.ON, + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_MODE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_PASS_APC_HEATING_PROFILE + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index d98709ba2b6b79..70477bbcdb2d8d 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -64,6 +64,7 @@ UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index c17f30393fc9be..85e5a3fdf571fe 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -27,7 +27,10 @@ def __init__( """Initialize the device.""" super().__init__(coordinator) self.device_url = device_url - self.base_device_url, *_ = self.device_url.split("#") + split_device_url = self.device_url.split("#") + self.base_device_url = split_device_url[0] + if len(split_device_url) == 2: + self.index_device_url = split_device_url[1] self.executor = OverkizExecutor(device_url, coordinator) self._attr_assumed_state = not self.device.states From ddbfed354e2a2d878e8c730059487961f97c847f Mon Sep 17 00:00:00 2001 From: Nyro Date: Fri, 4 Nov 2022 10:42:58 +0100 Subject: [PATCH 221/394] Add Overkiz AtlanticPassAPCDHW (#78665) * Add Overkiz AtlanticPassAPCDHW * Remove unnecessary line * Improve atlantic pass_apcdhw for operation and target temprature * Remove async_execute_commands * Fix small code issues for Overkiz AtlanticPassAPCDHW * Update homeassistant/components/overkiz/const.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py Co-authored-by: Quentame * Update homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py Co-authored-by: Quentame * Fix small issues Co-authored-by: Mick Vleeshouwer Co-authored-by: Quentame --- .coveragerc | 2 + homeassistant/components/overkiz/const.py | 2 + .../components/overkiz/water_heater.py | 28 ++++ .../overkiz/water_heater_entities/__init__.py | 8 + .../atlantic_pass_apc_dhw.py | 145 ++++++++++++++++++ 5 files changed, 185 insertions(+) create mode 100644 homeassistant/components/overkiz/water_heater.py create mode 100644 homeassistant/components/overkiz/water_heater_entities/__init__.py create mode 100644 homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py diff --git a/.coveragerc b/.coveragerc index eb60f320a749dd..84f061fbd35b16 100644 --- a/.coveragerc +++ b/.coveragerc @@ -953,6 +953,8 @@ omit = homeassistant/components/overkiz/sensor.py homeassistant/components/overkiz/siren.py homeassistant/components/overkiz/switch.py + homeassistant/components/overkiz/water_heater.py + homeassistant/components/overkiz/water_heater_entities/* homeassistant/components/ovo_energy/__init__.py homeassistant/components/ovo_energy/const.py homeassistant/components/ovo_energy/sensor.py diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 70477bbcdb2d8d..4645b058182aed 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -32,6 +32,7 @@ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.WATER_HEATER, ] IGNORED_OVERKIZ_DEVICES: list[UIClass | UIWidget] = [ @@ -64,6 +65,7 @@ UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_PASS_APC_DHW: Platform.WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) diff --git a/homeassistant/components/overkiz/water_heater.py b/homeassistant/components/overkiz/water_heater.py new file mode 100644 index 00000000000000..e22f442c2662e3 --- /dev/null +++ b/homeassistant/components/overkiz/water_heater.py @@ -0,0 +1,28 @@ +"""Support for Overkiz water heater devices.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import HomeAssistantOverkizData +from .const import DOMAIN +from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Overkiz DHW from a config entry.""" + data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + WIDGET_TO_WATER_HEATER_ENTITY[device.widget]( + device.device_url, data.coordinator + ) + for device in data.platforms[Platform.WATER_HEATER] + if device.widget in WIDGET_TO_WATER_HEATER_ENTITY + ) diff --git a/homeassistant/components/overkiz/water_heater_entities/__init__.py b/homeassistant/components/overkiz/water_heater_entities/__init__.py new file mode 100644 index 00000000000000..e03585da56d93c --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/__init__.py @@ -0,0 +1,8 @@ +"""Water heater entities for the Overkiz (by Somfy) integration.""" +from pyoverkiz.enums.ui import UIWidget + +from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW + +WIDGET_TO_WATER_HEATER_ENTITY = { + UIWidget.ATLANTIC_PASS_APC_DHW: AtlanticPassAPCDHW, +} diff --git a/homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py b/homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py new file mode 100644 index 00000000000000..7c2ea6ff2d8d64 --- /dev/null +++ b/homeassistant/components/overkiz/water_heater_entities/atlantic_pass_apc_dhw.py @@ -0,0 +1,145 @@ +"""Support for Atlantic Pass APC DHW.""" + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.water_heater import ( + STATE_ECO, + STATE_HEAT_PUMP, + STATE_OFF, + STATE_PERFORMANCE, + WaterHeaterEntity, + WaterHeaterEntityFeature, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..entity import OverkizEntity + + +class AtlanticPassAPCDHW(OverkizEntity, WaterHeaterEntity): + """Representation of Atlantic Pass APC DHW.""" + + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + | WaterHeaterEntityFeature.AWAY_MODE + ) + _attr_operation_list = [STATE_OFF, STATE_HEAT_PUMP, STATE_PERFORMANCE] + + @property + def target_temperature(self) -> float: + """Return the temperature corresponding to the PRESET.""" + if self.is_boost_mode_on: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_COMFORT_TARGET_DWH_TEMPERATURE + ), + ) + + if self.is_eco_mode_on: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_ECO_TARGET_DWH_TEMPERATURE + ), + ) + + return cast( + float, + self.executor.select_state(OverkizState.CORE_TARGET_DWH_TEMPERATURE), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + + if self.is_eco_mode_on: + await self.executor.async_execute_command( + OverkizCommand.SET_ECO_TARGET_DHW_TEMPERATURE, temperature + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_ECO_TARGET_DWH_TEMPERATURE + ) + else: + await self.executor.async_execute_command( + OverkizCommand.SET_COMFORT_TARGET_DHW_TEMPERATURE, temperature + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_COMFORT_TARGET_DWH_TEMPERATURE + ) + await self.executor.async_execute_command( + OverkizCommand.REFRESH_TARGET_DWH_TEMPERATURE + ) + + @property + def is_boost_mode_on(self) -> bool: + """Return true if boost mode is on.""" + return ( + self.executor.select_state(OverkizState.CORE_BOOST_ON_OFF) + == OverkizCommandParam.ON + ) + + @property + def is_eco_mode_on(self) -> bool: + """Return true if eco mode is on.""" + return ( + self.executor.select_state(OverkizState.IO_PASS_APCDWH_MODE) + == OverkizCommandParam.ECO + ) + + @property + def is_away_mode_on(self) -> bool: + """Return true if away mode is on.""" + return ( + self.executor.select_state(OverkizState.CORE_DWH_ON_OFF) + == OverkizCommandParam.OFF + ) + + @property + def current_operation(self) -> str: + """Return current operation.""" + if self.is_boost_mode_on: + return STATE_PERFORMANCE + if self.is_eco_mode_on: + return STATE_ECO + if self.is_away_mode_on: + return STATE_OFF + return STATE_HEAT_PUMP + + async def async_set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode.""" + boost_state = OverkizCommandParam.OFF + regular_state = OverkizCommandParam.OFF + if operation_mode == STATE_PERFORMANCE: + boost_state = OverkizCommandParam.ON + regular_state = OverkizCommandParam.ON + elif operation_mode == STATE_HEAT_PUMP: + regular_state = OverkizCommandParam.ON + + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_ON_OFF_STATE, boost_state + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_ON_OFF_STATE, regular_state + ) + + async def async_turn_away_mode_on(self) -> None: + """Turn away mode on.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_ON_OFF_STATE, OverkizCommandParam.OFF + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_ON_OFF_STATE, OverkizCommandParam.OFF + ) + + async def async_turn_away_mode_off(self) -> None: + """Turn away mode off.""" + await self.executor.async_execute_command( + OverkizCommand.SET_BOOST_ON_OFF_STATE, OverkizCommandParam.OFF + ) + await self.executor.async_execute_command( + OverkizCommand.SET_DHW_ON_OFF_STATE, OverkizCommandParam.ON + ) From 2e92a0d1c2f5657ac5e7a7cd0313a7f980808505 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Nov 2022 14:42:37 +0100 Subject: [PATCH 222/394] Bump oralb-ble to 0.10.2 (#81537) Fixes some more missing pressure mappings changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.1...v0.10.2 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 520306aed037d2..ba89c73a240fef 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.1"], + "requirements": ["oralb-ble==0.10.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 5a866deb8a6200..2515838e131fdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1244,7 +1244,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.1 +oralb-ble==0.10.2 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6e084742173af..f5ccd0c0b06268 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.1 +oralb-ble==0.10.2 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 52c80b7c5b2ae437710693c4b671675dda08851d Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Fri, 4 Nov 2022 14:54:19 +0100 Subject: [PATCH 223/394] Add Tuya Backlight mode configuration (#81218) * Tuya backlight configuration * fix codespell --- .../components/zha/core/channels/general.py | 1 + homeassistant/components/zha/select.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index c028a6021da9c5..de68ed7d8ef11b 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -352,6 +352,7 @@ def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name self.ZCL_INIT_ATTRS.copy() ) + self.ZCL_INIT_ATTRS["backlight_mode"] = True self.ZCL_INIT_ATTRS["power_on_state"] = True @property diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 5ac0ec6d16408f..334040a2b4d9b5 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -269,6 +269,26 @@ class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_stat _attr_name = "Power on state" +class TuyaBacklightMode(types.enum8): + """Tuya switch backlight mode enum.""" + + Off = 0x00 + LightWhenOn = 0x01 + LightWhenOff = 0x02 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_ON_OFF, + models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"}, +) +class TuyaBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"): + """Representation of a ZHA backlight mode select entity.""" + + _select_attr = "backlight_mode" + _enum = TuyaBacklightMode + _attr_name = "Backlight mode" + + class MoesBacklightMode(types.enum8): """MOES switch backlight mode enum.""" From ca905a8c054fbab18b58428cab99f6e7b06ff7ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Nov 2022 15:29:15 +0100 Subject: [PATCH 224/394] Bump dbus-fast to 1.71.0 (#81541) performance improvements changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.67.0...v1.71.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 49731df2f9bcdf..c47675f3a81f0e 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.6.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.67.0" + "dbus-fast==1.71.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae7e535dbc0a04..7250822ec22180 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.3 -dbus-fast==1.67.0 +dbus-fast==1.71.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2515838e131fdc..b2c40f90ab7f08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,7 +546,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.67.0 +dbus-fast==1.71.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5ccd0c0b06268..9f7f3e34bfc2f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.67.0 +dbus-fast==1.71.0 # homeassistant.components.debugpy debugpy==1.6.3 From 59ec52a0798e27b015b4e04f73482a1645a514f5 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 4 Nov 2022 18:29:00 +0200 Subject: [PATCH 225/394] Fix Shelly Plus HT missing battery entity (#81564) --- homeassistant/components/shelly/sensor.py | 8 +------- homeassistant/components/shelly/utils.py | 7 ------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 3ddabf7ca2b7ac..cf1eb7508c7de5 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -47,12 +47,7 @@ async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import ( - get_device_entry_gen, - get_device_uptime, - is_rpc_device_externally_powered, - temperature_unit, -) +from .utils import get_device_entry_gen, get_device_uptime, temperature_unit @dataclass @@ -407,7 +402,6 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): value=lambda status, _: status["percent"], device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - removal_condition=is_rpc_device_externally_powered, entity_registry_enabled_default=True, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index c3b6d24752f591..a13d84d32bea06 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -364,13 +364,6 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool: return con_types is not None and con_types[channel].lower().startswith("light") -def is_rpc_device_externally_powered( - config: dict[str, Any], status: dict[str, Any], key: str -) -> bool: - """Return true if device has external power instead of battery.""" - return cast(bool, status[key]["external"]["present"]) - - def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: """Return list of input triggers for RPC device.""" triggers = [] From 0311063c4417e70b566eef8e4156c362fec1a199 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 5 Nov 2022 00:28:23 +0000 Subject: [PATCH 226/394] [ci skip] Translation update --- .../components/airq/translations/bg.json | 22 +++++++++ .../components/airq/translations/ca.json | 22 +++++++++ .../components/airq/translations/de.json | 22 +++++++++ .../components/airq/translations/es.json | 22 +++++++++ .../components/airq/translations/et.json | 22 +++++++++ .../components/airq/translations/no.json | 22 +++++++++ .../components/airq/translations/pt-BR.json | 22 +++++++++ .../components/airq/translations/ru.json | 22 +++++++++ .../components/airq/translations/zh-Hant.json | 22 +++++++++ .../components/bluetooth/translations/fr.json | 3 ++ .../fireservicerota/translations/fr.json | 5 ++ .../forecast_solar/translations/de.json | 2 +- .../forecast_solar/translations/es.json | 2 +- .../forecast_solar/translations/et.json | 2 +- .../forecast_solar/translations/no.json | 2 +- .../forecast_solar/translations/pt-BR.json | 4 +- .../forecast_solar/translations/zh-Hant.json | 2 +- .../components/hassio/translations/ca.json | 48 +++++++++++++++++++ .../huawei_lte/translations/id.json | 2 +- 19 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/airq/translations/bg.json create mode 100644 homeassistant/components/airq/translations/ca.json create mode 100644 homeassistant/components/airq/translations/de.json create mode 100644 homeassistant/components/airq/translations/es.json create mode 100644 homeassistant/components/airq/translations/et.json create mode 100644 homeassistant/components/airq/translations/no.json create mode 100644 homeassistant/components/airq/translations/pt-BR.json create mode 100644 homeassistant/components/airq/translations/ru.json create mode 100644 homeassistant/components/airq/translations/zh-Hant.json diff --git a/homeassistant/components/airq/translations/bg.json b/homeassistant/components/airq/translations/bg.json new file mode 100644 index 00000000000000..df43ab876baa6b --- /dev/null +++ b/homeassistant/components/airq/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_input": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u0438\u043b\u0438 mDNS \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438 \u043d\u0435\u0433\u043e\u0432\u0430\u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", + "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/ca.json b/homeassistant/components/airq/translations/ca.json new file mode 100644 index 00000000000000..b9784605a9b9c5 --- /dev/null +++ b/homeassistant/components/airq/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_input": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP", + "password": "Contrasenya" + }, + "description": "Proporciona l'adre\u00e7a IP o el mDNS del dispositiu i la seva contrasenya", + "title": "Identificaci\u00f3 del dispositiu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/de.json b/homeassistant/components/airq/translations/de.json new file mode 100644 index 00000000000000..bccfc24be6c23a --- /dev/null +++ b/homeassistant/components/airq/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_input": "Ung\u00fcltiger Hostname oder IP-Adresse" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse", + "password": "Passwort" + }, + "description": "Gib die IP-Adresse oder den mDNS des Ger\u00e4ts und sein Passwort an", + "title": "Identifizieren des Ger\u00e4ts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/es.json b/homeassistant/components/airq/translations/es.json new file mode 100644 index 00000000000000..e3a543daecb739 --- /dev/null +++ b/homeassistant/components/airq/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_input": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a" + }, + "description": "Proporciona la direcci\u00f3n IP o mDNS del dispositivo y su contrase\u00f1a", + "title": "Identificar el dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/et.json b/homeassistant/components/airq/translations/et.json new file mode 100644 index 00000000000000..7045d3d5278972 --- /dev/null +++ b/homeassistant/components/airq/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_input": "Hostinimi v\u00f5i IP aadress vigane" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress", + "password": "Salas\u00f5na" + }, + "description": "Sisesta seadme IP-aadress v\u00f5i mDNS ja parool", + "title": "Seadme tuvastamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/no.json b/homeassistant/components/airq/translations/no.json new file mode 100644 index 00000000000000..00f070772945db --- /dev/null +++ b/homeassistant/components/airq/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_input": "Ugyldig vertsnavn eller IP-adresse" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse", + "password": "Passord" + }, + "description": "Oppgi IP-adressen eller mDNS til enheten og passordet", + "title": "Identifiser enheten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/pt-BR.json b/homeassistant/components/airq/translations/pt-BR.json new file mode 100644 index 00000000000000..bf556a86b0d583 --- /dev/null +++ b/homeassistant/components/airq/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_input": "Nome do host ou endere\u00e7o IP inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "password": "Senha" + }, + "description": "Forne\u00e7a o endere\u00e7o IP ou mDNS do dispositivo e sua senha", + "title": "Identifique o dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/ru.json b/homeassistant/components/airq/translations/ru.json new file mode 100644 index 00000000000000..416787acc7f9a8 --- /dev/null +++ b/homeassistant/components/airq/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_input": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 mDNS \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u0435\u0433\u043e \u043f\u0430\u0440\u043e\u043b\u044c.", + "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/zh-Hant.json b/homeassistant/components/airq/translations/zh-Hant.json new file mode 100644 index 00000000000000..db4d2d8fcf0cfe --- /dev/null +++ b/homeassistant/components/airq/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_input": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc" + }, + "description": "\u63d0\u4f9b\u88dd\u7f6e\u4e4b IP \u4f4d\u5740\u6216 mDNS \u53ca\u5bc6\u78bc", + "title": "\u8b58\u5225\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json index c7a1155b216f9b..025b00bb0cff17 100644 --- a/homeassistant/components/bluetooth/translations/fr.json +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -17,6 +17,9 @@ }, "description": "S\u00e9lectionner un adaptateur Bluetooth \u00e0 configurer" }, + "single_adapter": { + "description": "Voulez-vous configurer l\u2019adaptateur Bluetooth {name}\u00a0?" + }, "user": { "data": { "address": "Appareil" diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json index bf663e8ad900ae..835af421158514 100644 --- a/homeassistant/components/fireservicerota/translations/fr.json +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -17,6 +17,11 @@ }, "description": "Les jetons d'authentification ne sont plus valides, connectez-vous pour les recr\u00e9er." }, + "reauth_confirm": { + "data": { + "password": "Mot de passe" + } + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/forecast_solar/translations/de.json b/homeassistant/components/forecast_solar/translations/de.json index 06e51e0465925f..4e683878c111dd 100644 --- a/homeassistant/components/forecast_solar/translations/de.json +++ b/homeassistant/components/forecast_solar/translations/de.json @@ -25,7 +25,7 @@ "inverter_size": "Wechselrichtergr\u00f6\u00dfe (Watt)", "modules power": "Gesamt-Watt-Spitzenleistung deiner Solarmodule" }, - "description": "Mit diesen Werten kann das Solar.Forecast-Ergebnis angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach." + "description": "Mit diesen Werten kann das Ergebnis von Forecast.Solar angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach." } } } diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 08e67ec95d1dc8..a2f08dbc489119 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -25,7 +25,7 @@ "inverter_size": "Tama\u00f1o del inversor (vatios)", "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares" }, - "description": "Estos valores permiten modificar el resultado de Solar.Forecast. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." + "description": "Estos valores permiten modificar el resultado de Forecast.Solar. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } } diff --git a/homeassistant/components/forecast_solar/translations/et.json b/homeassistant/components/forecast_solar/translations/et.json index d2b72b3070826d..3a3b3f4c6890c9 100644 --- a/homeassistant/components/forecast_solar/translations/et.json +++ b/homeassistant/components/forecast_solar/translations/et.json @@ -25,7 +25,7 @@ "inverter_size": "Inverteri v\u00f5imsus (vatti)", "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides" }, - "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui asi on ebaselge." + "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui see v\u00e4li on ebaselge." } } } diff --git a/homeassistant/components/forecast_solar/translations/no.json b/homeassistant/components/forecast_solar/translations/no.json index a9acbb86f00a4d..6b3a58574a8a5a 100644 --- a/homeassistant/components/forecast_solar/translations/no.json +++ b/homeassistant/components/forecast_solar/translations/no.json @@ -25,7 +25,7 @@ "inverter_size": "Inverterst\u00f8rrelse (Watt)", "modules power": "Total Watt-toppeffekt i solcellemodulene dine" }, - "description": "Disse verdiene tillater justering av Solar.Forecast -resultatet. Se dokumentasjonen hvis et felt er uklart." + "description": "Disse verdiene gj\u00f8r det mulig \u00e5 justere Forecast.Solar-resultatet. Vennligst se dokumentasjonen hvis et felt er uklart." } } } diff --git a/homeassistant/components/forecast_solar/translations/pt-BR.json b/homeassistant/components/forecast_solar/translations/pt-BR.json index 6761e17e8bdc0f..a6be7f053e1eb9 100644 --- a/homeassistant/components/forecast_solar/translations/pt-BR.json +++ b/homeassistant/components/forecast_solar/translations/pt-BR.json @@ -10,7 +10,7 @@ "modules power": "Pot\u00eancia de pico total em Watt de seus m\u00f3dulos solares", "name": "Nome" }, - "description": "Preencha os dados de seus pain\u00e9is solares. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." + "description": "Esses valores permitem ajustar o resultado do Forecast.Solar. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." } } }, @@ -25,7 +25,7 @@ "inverter_size": "Pot\u00eancia do inversor (Watt)", "modules power": "Pot\u00eancia de pico total em Watt de seus m\u00f3dulos solares" }, - "description": "Preencha os dados de seus pain\u00e9is solares. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." + "description": "Esses valores permitem ajustar o resultado do Forecast.Solar. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." } } } diff --git a/homeassistant/components/forecast_solar/translations/zh-Hant.json b/homeassistant/components/forecast_solar/translations/zh-Hant.json index 3870ca298468fa..ede97887887891 100644 --- a/homeassistant/components/forecast_solar/translations/zh-Hant.json +++ b/homeassistant/components/forecast_solar/translations/zh-Hant.json @@ -25,7 +25,7 @@ "inverter_size": "\u8b8a\u6d41\u5668\u5c3a\u5bf8\uff08Watt\uff09", "modules power": "\u7e3d\u5cf0\u503c\u529f\u7387" }, - "description": "\u6b64\u4e9b\u6578\u503c\u5141\u8a31\u5fae\u8abf Solar.Forecast \u7d50\u679c\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\u3001\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002" + "description": "\u6b64\u4e9b\u6578\u503c\u5141\u8a31\u5fae\u8abf Forecast.Solar \u7d50\u679c\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\u3001\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002" } } } diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 40a1c9435aae07..13e928e998ba02 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -43,6 +43,54 @@ "unsupported_docker_version": { "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 de Docker incorrecta. Clica l'enlla\u00e7 per con\u00e8ixer la versi\u00f3 correcta i sobre com solucionar-ho.", "title": "Sistema no compatible - Versi\u00f3 de Docker" + }, + "unsupported_job_conditions": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 una o m\u00e9s condicions de treball ('job conditions'), que protegeixen de fallades i errors inesperats, s'han desactivat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Proteccions desactivades" + }, + "unsupported_lxc": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'est\u00e0 executant en una m\u00e0quina virtual LXC. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - LXC detectat" + }, + "unsupported_network_manager": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el gestor de xarxa ('Network Manager') no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb el gestor de xarxa" + }, + "unsupported_os": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el sistema operatiu utilitzat no s'ha provat o no est\u00e0 fet per utilitzar-se amb el Supervisor. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre els sitemes operatius compatibles i com solucionar-ho.", + "title": "Sistema no compatible - Sistema Operatiu" + }, + "unsupported_os_agent": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 OS-Agent no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb OS-Agent" + }, + "unsupported_restart_policy": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 un contenidor Docker t\u00e9 una pol\u00edtica de reinici que podria provocar problemes en l'arrencada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Pol\u00edtica de reinici dels contenidors" + }, + "unsupported_software": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'ha detectat programari extern al sistema Home Assistant. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Programari no compatible" + }, + "unsupported_source_mods": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'ha modificat el codi font del Supervisor. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Modificaci\u00f3 de codi font del Supervisor" + }, + "unsupported_supervisor_version": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 del Supervisor obsoleta i l'actualitzaci\u00f3 autom\u00e0tica est\u00e0 desactivada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Versi\u00f3 del Supervisor" + }, + "unsupported_systemd": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Systemd no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb Systemd" + }, + "unsupported_systemd_journal": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Systemd Journal i/o el servei d'enlla\u00e7 ('gateway') no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb Systemd Journal" + }, + "unsupported_systemd_resolved": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Systemd Resolved no est\u00e0 instal\u00b7lat, no est\u00e0 activat o no est\u00e0 ben configurat. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb Systemd-Resolved" } }, "system_health": { diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index 5bb08d626d03d9..d87b2bba3398c0 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -39,7 +39,7 @@ "step": { "init": { "data": { - "name": "Nama layanan notifikasi (perubahan harus dimulai ulang)", + "name": "Nama layanan notifikasi (perubahan membutuhkan proses mulai ulang)", "recipient": "Penerima notifikasi SMS", "track_wired_clients": "Lacak klien jaringan kabel", "unauthenticated_mode": "Mode tidak diautentikasi (perubahan memerlukan pemuatan ulang)" From 9a747bafa398185eb3d4fe041c52acfbb8264372 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 5 Nov 2022 05:33:56 -0400 Subject: [PATCH 227/394] Use enums instead of deprecated constants (#81591) --- .../components/eight_sleep/sensor.py | 6 +- homeassistant/components/tomorrowio/sensor.py | 38 +++++----- .../components/tomorrowio/weather.py | 19 +++-- homeassistant/components/zwave_js/climate.py | 15 ++-- .../zwave_js/discovery_data_template.py | 71 ++++++++----------- 5 files changed, 67 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index b07865d8591e09..7cce1293707372 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform as ep from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -175,7 +175,7 @@ def __init__( if self._sensor == "bed_temperature": self._attr_icon = "mdi:thermometer" self._attr_device_class = SensorDeviceClass.TEMPERATURE - self._attr_native_unit_of_measurement = TEMP_CELSIUS + self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS elif self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"): self._attr_native_unit_of_measurement = "Score" @@ -272,7 +272,7 @@ class EightRoomSensor(EightSleepBaseEntity, SensorEntity): _attr_icon = "mdi:thermometer" _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS + _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS def __init__( self, diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index 07b922e72ed7e6..a174d983131011 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -26,13 +26,11 @@ CONF_NAME, IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, IRRADIATION_WATTS_PER_SQUARE_METER, - LENGTH_KILOMETERS, - LENGTH_MILES, PERCENTAGE, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -103,20 +101,20 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa TomorrowioSensorEntityDescription( key=TMRW_ATTR_FEELS_LIKE, name="Feels Like", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), TomorrowioSensorEntityDescription( key=TMRW_ATTR_DEW_POINT, name="Dew Point", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), # Data comes in as hPa TomorrowioSensorEntityDescription( key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL, name="Pressure (Surface Level)", - native_unit_of_measurement=PRESSURE_HPA, + native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, ), # Data comes in as W/m^2, convert to BTUs/(hr * ft^2) for imperial @@ -132,20 +130,24 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_BASE, name="Cloud Base", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, + unit_imperial=UnitOfLength.MILES, + unit_metric=UnitOfLength.KILOMETERS, imperial_conversion=lambda val: DistanceConverter.convert( - val, LENGTH_KILOMETERS, LENGTH_MILES + val, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, ), ), # Data comes in as km, convert to miles for imperial TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_CEILING, name="Cloud Ceiling", - unit_imperial=LENGTH_MILES, - unit_metric=LENGTH_KILOMETERS, + unit_imperial=UnitOfLength.MILES, + unit_metric=UnitOfLength.KILOMETERS, imperial_conversion=lambda val: DistanceConverter.convert( - val, LENGTH_KILOMETERS, LENGTH_MILES + val, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, ), ), TomorrowioSensorEntityDescription( @@ -157,10 +159,10 @@ def convert_ppb_to_ugm3(molecular_weight: int | float) -> Callable[[float], floa TomorrowioSensorEntityDescription( key=TMRW_ATTR_WIND_GUST, name="Wind Gust", - unit_imperial=SPEED_MILES_PER_HOUR, - unit_metric=SPEED_METERS_PER_SECOND, + unit_imperial=UnitOfSpeed.MILES_PER_HOUR, + unit_metric=UnitOfSpeed.METERS_PER_SECOND, imperial_conversion=lambda val: SpeedConverter.convert( - val, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR + val, UnitOfSpeed.METERS_PER_SECOND, UnitOfSpeed.MILES_PER_HOUR ), ), TomorrowioSensorEntityDescription( diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 07ea079b1cee94..1acc42e900b048 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -21,11 +21,10 @@ from homeassistant.const import ( CONF_API_KEY, CONF_NAME, - LENGTH_KILOMETERS, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -74,11 +73,11 @@ async def async_setup_entry( class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_visibility_unit = LENGTH_KILOMETERS - _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_precipitation_unit = UnitOfLength.MILLIMETERS + _attr_native_pressure_unit = UnitOfPressure.HPA + _attr_native_temperature_unit = UnitOfTemperature.CELSIUS + _attr_native_visibility_unit = UnitOfLength.KILOMETERS + _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND def __init__( self, diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 6cbb1ea3016c1c..06f2d22574222d 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -34,12 +34,7 @@ HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TEMPERATURE, - PRECISION_TENTHS, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -260,8 +255,8 @@ def temperature_unit(self) -> str: and self._unit_value.metadata.unit and "f" in self._unit_value.metadata.unit.lower() ): - return TEMP_FAHRENHEIT - return TEMP_CELSIUS + return UnitOfTemperature.FAHRENHEIT + return UnitOfTemperature.CELSIUS @property def hvac_mode(self) -> HVACMode: @@ -398,7 +393,7 @@ def extra_state_attributes(self) -> dict[str, str] | None: def min_temp(self) -> float: """Return the minimum temperature.""" min_temp = DEFAULT_MIN_TEMP - base_unit = TEMP_CELSIUS + base_unit: str = UnitOfTemperature.CELSIUS try: temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.min: @@ -414,7 +409,7 @@ def min_temp(self) -> float: def max_temp(self) -> float: """Return the maximum temperature.""" max_temp = DEFAULT_MAX_TEMP - base_unit = TEMP_CELSIUS + base_unit: str = UnitOfTemperature.CELSIUS try: temp = self._setpoint_value_or_raise(self._current_mode_setpoint_enums[0]) if temp.metadata.max: diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 5b20572c2d845b..0ee7c3d758e6b7 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -96,35 +96,24 @@ ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_MILLIVOLT, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, FREQUENCY_HERTZ, FREQUENCY_KILOHERTZ, IRRADIATION_WATTS_PER_SQUARE_METER, - LENGTH_CENTIMETERS, - LENGTH_FEET, - LENGTH_METERS, LIGHT_LUX, - MASS_KILOGRAMS, - MASS_POUNDS, PERCENTAGE, - POWER_BTU_PER_HOUR, - POWER_WATT, - PRESSURE_INHG, - PRESSURE_MMHG, - PRESSURE_PSI, SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, TIME_SECONDS, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, - VOLUME_GALLONS, - VOLUME_LITERS, + UnitOfEnergy, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolume, UnitOfVolumetricFlux, ) @@ -173,21 +162,21 @@ METER_UNIT_MAP: dict[str, set[MeterScaleType]] = { ELECTRIC_CURRENT_AMPERE: METER_UNIT_AMPERE, - VOLUME_CUBIC_FEET: UNIT_CUBIC_FEET, - VOLUME_CUBIC_METERS: METER_UNIT_CUBIC_METER, - VOLUME_GALLONS: UNIT_US_GALLON, - ENERGY_KILO_WATT_HOUR: UNIT_KILOWATT_HOUR, + UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS: METER_UNIT_CUBIC_METER, + UnitOfVolume.GALLONS: UNIT_US_GALLON, + UnitOfEnergy.KILO_WATT_HOUR: UNIT_KILOWATT_HOUR, ELECTRIC_POTENTIAL_VOLT: METER_UNIT_VOLT, - POWER_WATT: METER_UNIT_WATT, + UnitOfPower.WATT: METER_UNIT_WATT, } MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = { ELECTRIC_CURRENT_AMPERE: SENSOR_UNIT_AMPERE, - POWER_BTU_PER_HOUR: UNIT_BTU_H, - TEMP_CELSIUS: UNIT_CELSIUS, - LENGTH_CENTIMETERS: UNIT_CENTIMETER, + UnitOfPower.BTU_PER_HOUR: UNIT_BTU_H, + UnitOfTemperature.CELSIUS: UNIT_CELSIUS, + UnitOfLength.CENTIMETERS: UNIT_CENTIMETER, VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE, - VOLUME_CUBIC_METERS: SENSOR_UNIT_CUBIC_METER, + UnitOfVolume.CUBIC_METERS: SENSOR_UNIT_CUBIC_METER, VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR, SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL, DEGREE: UNIT_DEGREES, @@ -195,31 +184,31 @@ *UNIT_DENSITY, *UNIT_MICROGRAM_PER_CUBIC_METER, }, - TEMP_FAHRENHEIT: UNIT_FAHRENHEIT, - LENGTH_FEET: UNIT_FEET, - VOLUME_GALLONS: UNIT_GALLONS, + UnitOfTemperature.FAHRENHEIT: UNIT_FAHRENHEIT, + UnitOfLength.FEET: UNIT_FEET, + UnitOfVolume.GALLONS: UNIT_GALLONS, FREQUENCY_HERTZ: UNIT_HERTZ, - PRESSURE_INHG: UNIT_INCHES_OF_MERCURY, + UnitOfPressure.INHG: UNIT_INCHES_OF_MERCURY, UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR, - MASS_KILOGRAMS: UNIT_KILOGRAM, + UnitOfMass.KILOGRAMS: UNIT_KILOGRAM, FREQUENCY_KILOHERTZ: UNIT_KILOHERTZ, - VOLUME_LITERS: UNIT_LITER, + UnitOfVolume.LITERS: UNIT_LITER, LIGHT_LUX: UNIT_LUX, - LENGTH_METERS: UNIT_METER, + UnitOfLength.METERS: UNIT_METER, ELECTRIC_CURRENT_MILLIAMPERE: UNIT_MILLIAMPERE, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR: UNIT_MILLIMETER_HOUR, ELECTRIC_POTENTIAL_MILLIVOLT: UNIT_MILLIVOLT, - SPEED_MILES_PER_HOUR: UNIT_MPH, - SPEED_METERS_PER_SECOND: UNIT_M_S, + UnitOfSpeed.MILES_PER_HOUR: UNIT_MPH, + UnitOfSpeed.METERS_PER_SECOND: UNIT_M_S, CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION, PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI}, - MASS_POUNDS: UNIT_POUNDS, - PRESSURE_PSI: UNIT_POUND_PER_SQUARE_INCH, + UnitOfMass.POUNDS: UNIT_POUNDS, + UnitOfPressure.PSI: UNIT_POUND_PER_SQUARE_INCH, SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL, TIME_SECONDS: UNIT_SECOND, - PRESSURE_MMHG: UNIT_SYSTOLIC, + UnitOfPressure.MMHG: UNIT_SYSTOLIC, ELECTRIC_POTENTIAL_VOLT: SENSOR_UNIT_VOLT, - POWER_WATT: SENSOR_UNIT_WATT, + UnitOfPower.WATT: SENSOR_UNIT_WATT, IRRADIATION_WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER, } From 089c4a7da2a955b177015d3189231736d9fb89cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 04:44:05 -0500 Subject: [PATCH 228/394] Bump bluetooth-adapters to 0.7.0 (#81576) changelog: https://github.com/Bluetooth-Devices/bluetooth-adapters/releases/tag/v0.7.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c47675f3a81f0e..0b94e1bdfd9e15 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -8,7 +8,7 @@ "requirements": [ "bleak==0.19.1", "bleak-retry-connector==2.8.2", - "bluetooth-adapters==0.6.0", + "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.71.0" ], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7250822ec22180..b5b7350d4dbbb8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.8.2 bleak==0.19.1 -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b2c40f90ab7f08..660366902edd2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -444,7 +444,7 @@ bluemaestro-ble==0.2.0 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f7f3e34bfc2f9..36710491d155fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -358,7 +358,7 @@ blinkpy==0.19.2 bluemaestro-ble==0.2.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.6.0 +bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 From 64a508be7bc5034d5786caea0238512d0599e6e4 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 5 Nov 2022 21:05:19 +1100 Subject: [PATCH 229/394] Add integration_type to geonetnz_quakes (#81548) define integration type --- homeassistant/components/geonetnz_quakes/manifest.json | 3 ++- homeassistant/generated/integrations.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index ba8eecc4ae9202..dd62682e304871 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@exxamalte"], "quality_scale": "platinum", "iot_class": "cloud_polling", - "loggers": ["aio_geojson_geonetnz_quakes"] + "loggers": ["aio_geojson_geonetnz_quakes"], + "integration_type": "service" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 4d1b9da50af12c..55a330685d507e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1835,7 +1835,7 @@ "name": "GeoNet", "integrations": { "geonetnz_quakes": { - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "GeoNet NZ Quakes" From 83c6a7e18b1b0e4d5a302e304f117dee11d3aa51 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 5 Nov 2022 08:40:28 -0400 Subject: [PATCH 230/394] Fix invalid min and max color temp in bad ZHA light devices (#81604) * Fix ZHA default color temps * update test --- .../components/zha/core/channels/lighting.py | 18 ++++++++++++++++-- tests/components/zha/test_light.py | 6 +++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index e70eea11a8742a..ffbbc32a7a51ff 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -98,12 +98,26 @@ def current_saturation(self) -> int | None: @property def min_mireds(self) -> int: """Return the coldest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS) + min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS) + if min_mireds == 0: + self.warning( + "[Min mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected", + self.MIN_MIREDS, + ) + min_mireds = self.MIN_MIREDS + return min_mireds @property def max_mireds(self) -> int: """Return the warmest color_temp that this channel supports.""" - return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + if max_mireds == 0: + self.warning( + "[Max mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected", + self.MAX_MIREDS, + ) + max_mireds = self.MAX_MIREDS + return max_mireds @property def hs_supported(self) -> bool: diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index a9b8c7a14ee89a..d40605f81dc41b 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -242,7 +242,9 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature - | lighting.Color.ColorCapabilities.XY_attributes + | lighting.Color.ColorCapabilities.XY_attributes, + "color_temp_physical_min": 0, + "color_temp_physical_max": 0, } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -1192,6 +1194,8 @@ async def test_transitions( assert eWeLink_state.state == STATE_ON assert eWeLink_state.attributes["color_temp"] == 235 assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + assert eWeLink_state.attributes["min_mireds"] == 153 + assert eWeLink_state.attributes["max_mireds"] == 500 async def async_test_on_off_from_light(hass, cluster, entity_id): From b313f3794692fd5edd97e5637efabb1efeeff14d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 09:55:43 -0500 Subject: [PATCH 231/394] Bump nexia to 2.0.6 (#81474) * Bump nexia to 2.0.6 - Marks thermostat unavailable when it is offline * is property --- homeassistant/components/nexia/entity.py | 5 +++++ homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 4f806d03edad7f..6b017db4d34b2a 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -80,6 +80,11 @@ def _signal_thermostat_update(self): self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" ) + @property + def available(self) -> bool: + """Return True if thermostat is available and data is available.""" + return super().available and self._thermostat.is_online + class NexiaThermostatZoneEntity(NexiaThermostatEntity): """Base class for nexia devices attached to a thermostat.""" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 78576e06b8aff0..99eb7c14798013 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.5"], + "requirements": ["nexia==2.0.6"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 660366902edd2c..156a47e9731d59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1141,7 +1141,7 @@ nettigo-air-monitor==1.5.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.5 +nexia==2.0.6 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36710491d155fa..a8e30ddc07cea7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -831,7 +831,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.5.0 # homeassistant.components.nexia -nexia==2.0.5 +nexia==2.0.6 # homeassistant.components.discord nextcord==2.0.0a8 From 883ac12bcbf252c9ee64afd3e78472fe7ae60b27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 09:57:32 -0500 Subject: [PATCH 232/394] Add additional coverage for adding multiple elkm1 instances (#81528) * Add additional coverage for adding multiple elkm1 instances * fix copy error --- tests/components/elkm1/test_config_flow.py | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index e47dc402b640bf..7ce0e2163ac30d 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -2,6 +2,7 @@ from dataclasses import asdict from unittest.mock import patch +from elkm1_lib.discovery import ElkSystem import pytest from homeassistant import config_entries @@ -1317,3 +1318,139 @@ async def test_discovered_by_dhcp_no_udp_response(hass): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" + + +async def test_multiple_instances_with_discovery(hass): + """Test we can setup a secure elk.""" + + elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601) + elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601) + + with _patch_discovery(device=elk_discovery_1): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_1.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.1", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # Now try to add another instance with the different discovery info + with _patch_discovery(device=elk_discovery_2): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_2.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeefe" + assert result3["data"] == { + "auto_configure": True, + "host": "elks://127.0.0.2", + "password": "test-password", + "prefix": "ddeefe", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Finally, try to add another instance manually with no discovery info + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "prefix": "guest_house", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "guest_house" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "prefix": "guest_house", + "username": "", + "password": "", + } + assert len(mock_setup_entry.mock_calls) == 1 From 24f218c46b350ec0f1e9f6a681ad144be0db271a Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 5 Nov 2022 08:57:57 -0600 Subject: [PATCH 233/394] Bump pylitterbot to 2022.11.0 (#81572) --- homeassistant/components/litterrobot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 0965670e569f47..6384df2f25a903 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.10.2"], + "requirements": ["pylitterbot==2022.11.0"], "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 156a47e9731d59..aba4a370d3cb08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1694,7 +1694,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.10.2 +pylitterbot==2022.11.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8e30ddc07cea7..3f124f3feeb7b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1195,7 +1195,7 @@ pylibrespot-java==0.1.1 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.10.2 +pylitterbot==2022.11.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.17.1 From 5884f50a82f8296f51a0ebf630b265e6dab6d264 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 5 Nov 2022 11:58:47 -0600 Subject: [PATCH 234/394] Bump pyairvisual to 2022.11.1 (#81556) --- homeassistant/components/airvisual/__init__.py | 10 +++------- homeassistant/components/airvisual/config_flow.py | 6 +++--- homeassistant/components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airvisual/test_config_flow.py | 6 +++--- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index a2a3d76c3db509..2a544edb20aadf 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -7,13 +7,9 @@ from typing import Any from pyairvisual import CloudAPI, NodeSamba -from pyairvisual.errors import ( - AirVisualError, - InvalidKeyError, - KeyExpiredError, - NodeProError, - UnauthorizedError, -) +from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 385c9f55753125..9510c938cb0d46 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -6,14 +6,14 @@ from typing import Any from pyairvisual import CloudAPI, NodeSamba -from pyairvisual.errors import ( - AirVisualError, +from pyairvisual.cloud_api import ( InvalidKeyError, KeyExpiredError, - NodeProError, NotFoundError, UnauthorizedError, ) +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index 73bbf0cd5893c5..ae9eeb270a8310 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==2022.07.0"], + "requirements": ["pyairvisual==2022.11.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"], diff --git a/requirements_all.txt b/requirements_all.txt index aba4a370d3cb08..126bc615b28e15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1439,7 +1439,7 @@ pyaftership==21.11.0 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==2022.07.0 +pyairvisual==2022.11.1 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f124f3feeb7b0..2d1fb93d2d2d04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1027,7 +1027,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==2022.07.0 +pyairvisual==2022.11.1 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index f97ee845dba323..7603917eddc943 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,14 +1,14 @@ """Define tests for the AirVisual config flow.""" from unittest.mock import patch -from pyairvisual.errors import ( - AirVisualError, +from pyairvisual.cloud_api import ( InvalidKeyError, KeyExpiredError, - NodeProError, NotFoundError, UnauthorizedError, ) +from pyairvisual.errors import AirVisualError +from pyairvisual.node import NodeProError import pytest from homeassistant import data_entry_flow From 3a33d3646644fbf01b0a6aeaafb98410d0f9842c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Nov 2022 21:23:36 +0100 Subject: [PATCH 235/394] Fix situation where deCONZ sensor platform setup would fail (#81629) * Fix situation where deCONZ sensor platform setup would fail * Don't use try --- homeassistant/components/deconz/sensor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 66c186e20d7f66..f1bd011803099e 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -89,6 +89,7 @@ class DeconzSensorDescriptionMixin(Generic[T]): """Required values when describing secondary sensor attributes.""" + supported_fn: Callable[[T], bool] update_key: str value_fn: Callable[[T], datetime | StateType] @@ -105,6 +106,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( DeconzSensorDescription[AirQuality]( key="air_quality", + supported_fn=lambda device: device.air_quality is not None, update_key="airquality", value_fn=lambda device: device.air_quality, instance_check=AirQuality, @@ -112,6 +114,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[AirQuality]( key="air_quality_ppb", + supported_fn=lambda device: device.air_quality_ppb is not None, update_key="airqualityppb", value_fn=lambda device: device.air_quality_ppb, instance_check=AirQuality, @@ -122,6 +125,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[Consumption]( key="consumption", + supported_fn=lambda device: device.consumption is not None, update_key="consumption", value_fn=lambda device: device.scaled_consumption, instance_check=Consumption, @@ -131,6 +135,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[Daylight]( key="daylight_status", + supported_fn=lambda device: True, update_key="status", value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status], instance_check=Daylight, @@ -139,12 +144,14 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[GenericStatus]( key="status", + supported_fn=lambda device: device.status is not None, update_key="status", value_fn=lambda device: device.status, instance_check=GenericStatus, ), DeconzSensorDescription[Humidity]( key="humidity", + supported_fn=lambda device: device.humidity is not None, update_key="humidity", value_fn=lambda device: device.scaled_humidity, instance_check=Humidity, @@ -154,6 +161,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[LightLevel]( key="light_level", + supported_fn=lambda device: device.light_level is not None, update_key="lightlevel", value_fn=lambda device: device.scaled_light_level, instance_check=LightLevel, @@ -163,6 +171,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[Power]( key="power", + supported_fn=lambda device: device.power is not None, update_key="power", value_fn=lambda device: device.power, instance_check=Power, @@ -172,6 +181,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[Pressure]( key="pressure", + supported_fn=lambda device: device.pressure is not None, update_key="pressure", value_fn=lambda device: device.pressure, instance_check=Pressure, @@ -181,6 +191,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[Temperature]( key="temperature", + supported_fn=lambda device: device.temperature is not None, update_key="temperature", value_fn=lambda device: device.scaled_temperature, instance_check=Temperature, @@ -190,6 +201,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[Time]( key="last_set", + supported_fn=lambda device: device.last_set is not None, update_key="lastset", value_fn=lambda device: dt_util.parse_datetime(device.last_set), instance_check=Time, @@ -197,6 +209,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[SensorResources]( key="battery", + supported_fn=lambda device: device.battery is not None, update_key="battery", value_fn=lambda device: device.battery, name_suffix="Battery", @@ -208,6 +221,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi ), DeconzSensorDescription[SensorResources]( key="internal_temperature", + supported_fn=lambda device: device.internal_temperature is not None, update_key="temperature", value_fn=lambda device: device.internal_temperature, name_suffix="Temperature", @@ -268,7 +282,7 @@ def async_add_sensor(_: EventType, sensor_id: str) -> None: continue no_sensor_data = False - if description.value_fn(sensor) is None: + if not description.supported_fn(sensor): no_sensor_data = True if description.instance_check is None: From 5f9f95602369fc3f1d6a2071600cc35259aa0192 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 5 Nov 2022 21:26:19 +0100 Subject: [PATCH 236/394] Bump plugwise to v0.25.7 (#81612) --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 7f3e979ab7d3d0..6bb1c941bf31f3 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.25.3"], + "requirements": ["plugwise==0.25.7"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 126bc615b28e15..46fc167d1f428e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1318,7 +1318,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.3 +plugwise==0.25.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d1fb93d2d2d04..fd4f33edf02888 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -945,7 +945,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.3 +plugwise==0.25.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 6495c65d102075aea11ed68ae293ffaf1dde5c4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 15:28:47 -0500 Subject: [PATCH 237/394] Align esphome ble client notify behavior to match BlueZ (#81463) --- .../components/esphome/bluetooth/client.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 72531a2503a88c..c6b60831577ce8 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -137,6 +137,7 @@ def _async_ble_device_disconnected(self) -> None: was_connected = self._is_connected self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call] self._is_connected = False + self._notify_cancels.clear() if self._disconnected_event: self._disconnected_event.set() self._disconnected_event = None @@ -463,12 +464,20 @@ def callback(sender: int, data: bytearray): UUID or directly by the BleakGATTCharacteristic object representing it. callback (function): The function to be called on notification. """ + ble_handle = characteristic.handle + if ble_handle in self._notify_cancels: + raise BleakError( + "Notifications are already enabled on " + f"service:{characteristic.service_uuid} " + f"characteristic:{characteristic.uuid} " + f"handle:{ble_handle}" + ) cancel_coro = await self._client.bluetooth_gatt_start_notify( self._address_as_int, - characteristic.handle, + ble_handle, lambda handle, data: callback(data), ) - self._notify_cancels[characteristic.handle] = cancel_coro + self._notify_cancels[ble_handle] = cancel_coro @api_error_as_bleak_error async def stop_notify( @@ -483,5 +492,7 @@ async def stop_notify( directly by the BleakGATTCharacteristic object representing it. """ characteristic = self._resolve_characteristic(char_specifier) - coro = self._notify_cancels.pop(characteristic.handle) - await coro() + # Do not raise KeyError if notifications are not enabled on this characteristic + # to be consistent with the behavior of the BlueZ backend + if coro := self._notify_cancels.pop(characteristic.handle, None): + await coro() From 89ebca759465c612a2eae7c393ecd10448b704cb Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Sat, 5 Nov 2022 13:29:20 -0700 Subject: [PATCH 238/394] Bump venstarcolortouch to 0.19 to fix API rev 3 devices (#81614) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 4a6eea28e2437c..ce40e53105a42e 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.18"], + "requirements": ["venstarcolortouch==0.19"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index 46fc167d1f428e..b8b0b62b6916de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2493,7 +2493,7 @@ vehicle==0.4.0 velbus-aio==2022.10.4 # homeassistant.components.venstar -venstarcolortouch==0.18 +venstarcolortouch==0.19 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd4f33edf02888..c4f438b3d34255 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1727,7 +1727,7 @@ vehicle==0.4.0 velbus-aio==2022.10.4 # homeassistant.components.venstar -venstarcolortouch==0.18 +venstarcolortouch==0.19 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From 64a29fddb417afeb31bc529b3ca55b71634759c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 15:58:11 -0500 Subject: [PATCH 239/394] Bump aiohomekit to 2.2.16 (#81621) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index b2aec75c3ad9f7..e4a2b5d79bb839 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.14"], + "requirements": ["aiohomekit==2.2.16"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index b8b0b62b6916de..ff5b72fbae4cc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,7 +174,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.14 +aiohomekit==2.2.16 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4f438b3d34255..88277960d5bbbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.14 +aiohomekit==2.2.16 # homeassistant.components.emulated_hue # homeassistant.components.http From f66009c77dd901c1124ef4b8c213d67f8f113f39 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 6 Nov 2022 08:46:00 +1100 Subject: [PATCH 240/394] Fix lifx.set_state so it works with kelvin and color_temp_kelvin and color names (#81515) --- homeassistant/components/lifx/util.py | 26 +++++- tests/components/lifx/test_light.py | 128 ++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py index 135e1a7e8e917e..6a9bff465ee4a5 100644 --- a/homeassistant/components/lifx/util.py +++ b/homeassistant/components/lifx/util.py @@ -14,8 +14,11 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP_KELVIN, ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_XY_COLOR, ) @@ -24,7 +27,7 @@ from homeassistant.helpers import device_registry as dr import homeassistant.util.color as color_util -from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT +from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT FIX_MAC_FW = AwesomeVersion("3.70") @@ -80,6 +83,17 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | """ hue, saturation, brightness, kelvin = [None] * 4 + if (color_name := kwargs.get(ATTR_COLOR_NAME)) is not None: + try: + hue, saturation = color_util.color_RGB_to_hs( + *color_util.color_name_to_rgb(color_name) + ) + except ValueError: + _LOGGER.warning( + "Got unknown color %s, falling back to neutral white", color_name + ) + hue, saturation = (0, 0) + if ATTR_HS_COLOR in kwargs: hue, saturation = kwargs[ATTR_HS_COLOR] elif ATTR_RGB_COLOR in kwargs: @@ -93,6 +107,13 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | saturation = int(saturation / 100 * 65535) kelvin = 3500 + if ATTR_KELVIN in kwargs: + _LOGGER.warning( + "The 'kelvin' parameter is deprecated. Please use 'color_temp_kelvin' for all service calls" + ) + kelvin = kwargs.pop(ATTR_KELVIN) + saturation = 0 + if ATTR_COLOR_TEMP_KELVIN in kwargs: kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN) saturation = 0 @@ -100,6 +121,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | if ATTR_BRIGHTNESS in kwargs: brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + if ATTR_BRIGHTNESS_PCT in kwargs: + brightness = convert_8_to_16(round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100)) + hsbk = [hue, saturation, brightness, kelvin] return None if hsbk == [None] * 4 else hsbk diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py index 6fe63b14b6a5f4..5a9b250034a571 100644 --- a/tests/components/lifx/test_light.py +++ b/tests/components/lifx/test_light.py @@ -21,11 +21,14 @@ ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, ATTR_COLOR_MODE, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, @@ -1397,6 +1400,131 @@ async def test_transitions_color_bulb(hass: HomeAssistant) -> None: bulb.set_color.reset_mock() +async def test_lifx_set_state_color(hass: HomeAssistant) -> None: + """Test lifx.set_state works with color names and RGB.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 2700] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + # brightness should convert from 8 to 16 bits + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 65535, 2700] + bulb.set_color.reset_mock() + + # brightness_pct should convert into 16 bit + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 90}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 59110, 2700] + bulb.set_color.reset_mock() + + # color name should turn into hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "red", ATTR_BRIGHTNESS_PCT: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 65535, 65535, 3500] + bulb.set_color.reset_mock() + + # unknown color name should reset back to neutral white, i.e. 3500K + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_NAME: "deepblack"}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 0, 32000, 3500] + bulb.set_color.reset_mock() + + # RGB should convert to hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (0, 255, 0)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [21845, 65535, 32000, 3500] + bulb.set_color.reset_mock() + + # XY should convert to hue, saturation + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.34, 0.339)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [5461, 5139, 32000, 3500] + bulb.set_color.reset_mock() + + +async def test_lifx_set_state_kelvin(hass: HomeAssistant) -> None: + """Test set_state works with old and new kelvin parameter names.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 255, ATTR_KELVIN: 3500}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100, ATTR_COLOR_TEMP_KELVIN: 2700}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 25700, 2700] + bulb.set_color.reset_mock() + + async def test_infrared_color_bulb(hass: HomeAssistant) -> None: """Test setting infrared with a color bulb.""" already_migrated_config_entry = MockConfigEntry( From a635e9c9d239cd81dde79b250d01fcf393bcfa1c Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sat, 5 Nov 2022 23:11:59 +0100 Subject: [PATCH 241/394] Fix repeating SSDP errors by checking address scope_ids and proper hostname (#81611) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 9d05b02000bf1a..e4620386b98f41 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 98ad81e653a46e..7c7a312159bc49 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 5f70e912d352c4..0a65a47bd232bc 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -8,7 +8,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.32.1" + "async-upnp-client==0.32.2" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 59d9d6ddad81fd..3b30146e7564a3 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.32.1"], + "requirements": ["async-upnp-client==0.32.2"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9b4151c35c5180..4c45b099193b91 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -3,7 +3,7 @@ "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 16fe2ae7700ec0..6c4505481350de 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b5b7350d4dbbb8..eb580796a97730 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ff5b72fbae4cc5..54c0db9a92d589 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 88277960d5bbbf..c95d21e4c1ff02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -313,7 +313,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.32.1 +async-upnp-client==0.32.2 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From 2ad1d3111930f89a95a7dd86badcd24837077da2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 5 Nov 2022 17:06:34 -0700 Subject: [PATCH 242/394] Bump gcal_sync to 4.0.0 (#81562) * Bump gcal_sync to 2.2.4 * Bump gcal sync to 4.0.0 * Add Calendar accessRole fields which are now required --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_config_flow.py | 2 +- tests/components/google/test_init.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index f6ebc665cd7506..9fc265fa287d0f 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==2.2.3", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 54c0db9a92d589..f54c88aad8ea57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.3 +gcal-sync==4.0.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c95d21e4c1ff02..7793088d47cf5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -547,7 +547,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==2.2.3 +gcal-sync==4.0.0 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index d8ddd6fe5886ae..bce3f4855c7a86 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -104,7 +104,7 @@ async def primary_calendar( """Fixture to return the primary calendar.""" mock_calendar_get( "primary", - {"id": primary_calendar_email, "summary": "Personal"}, + {"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"}, exc=primary_calendar_error, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 5e7696eec6881e..a2f16f778fdab0 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -768,7 +768,7 @@ async def test_assign_unique_id( mock_calendar_get( "primary", - {"id": EMAIL_ADDRESS, "summary": "Personal"}, + {"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "reader"}, ) mock_calendars_list({"items": [test_api_calendar]}) From fc472eb040892ac9ab13a9a7ee6d2ae5bfecce66 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 6 Nov 2022 00:27:15 +0000 Subject: [PATCH 243/394] [ci skip] Translation update --- .../accuweather/translations/ja.json | 2 +- .../components/airq/translations/it.json | 22 +++++++ .../components/airq/translations/nl.json | 22 +++++++ .../airthings_ble/translations/nl.json | 2 +- .../components/brother/translations/it.json | 2 +- .../components/dlna_dms/translations/it.json | 2 +- .../components/esphome/translations/it.json | 2 +- .../forecast_solar/translations/it.json | 2 +- .../components/fritz/translations/it.json | 2 +- .../components/generic/translations/it.json | 4 +- .../components/hassio/translations/ca.json | 4 ++ .../components/hue/translations/it.json | 2 +- .../components/insteon/translations/it.json | 2 +- .../components/ipp/translations/it.json | 2 +- .../components/melnor/translations/it.json | 2 +- .../components/mqtt/translations/it.json | 16 ++--- .../components/mqtt/translations/nl.json | 58 ++++++++++++++++--- .../components/mysensors/translations/nl.json | 4 +- .../components/nobo_hub/translations/it.json | 2 +- .../components/oralb/translations/it.json | 10 ++-- .../plugwise/translations/select.it.json | 4 +- .../pure_energie/translations/it.json | 2 +- .../components/pushover/translations/it.json | 2 +- .../sensibo/translations/sensor.it.json | 2 +- .../components/smappee/translations/it.json | 2 +- .../components/wled/translations/it.json | 2 +- .../xiaomi_ble/translations/nl.json | 20 +++++++ .../xiaomi_miio/translations/select.nl.json | 3 +- .../components/zha/translations/nl.json | 2 + .../components/zwave_js/translations/it.json | 2 +- 30 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 homeassistant/components/airq/translations/it.json create mode 100644 homeassistant/components/airq/translations/nl.json diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index b9c7819e78ad24..3583f9760b966c 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -28,7 +28,7 @@ "data": { "forecast": "\u5929\u6c17\u4e88\u5831" }, - "description": "\u5236\u9650\u306b\u3088\u308a\u7121\u6599\u30d0\u30fc\u30b8\u30e7\u30f3\u306eAccuWeather API\u30ad\u30fc\u3067\u306f\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u3067\u306f\u306a\u304f80\u5206\u3054\u3068\u3067\u3059\u3002" + "description": "\u7121\u6599\u7248\u306eAccuWeather API\u30ad\u30fc\u306e\u5236\u9650\u306b\u3088\u308a\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u305f\u5834\u5408\u3001\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u6bce\u3067\u306f\u306a\u304f80\u5206\u6bce\u306b\u5b9f\u884c\u3055\u308c\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/airq/translations/it.json b/homeassistant/components/airq/translations/it.json new file mode 100644 index 00000000000000..782f631bb0987c --- /dev/null +++ b/homeassistant/components/airq/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_input": "Nome host o indirizzo IP non valido" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP", + "password": "Password" + }, + "description": "Fornire l'indirizzo IP o mDNS del dispositivo e la relativa password", + "title": "Identifica il dispositivo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/nl.json b/homeassistant/components/airq/translations/nl.json new file mode 100644 index 00000000000000..07fe49d370864f --- /dev/null +++ b/homeassistant/components/airq/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinding mislukt", + "invalid_auth": "Ongeldige authenticatie poging", + "invalid_input": "Ongeldige hostnaam of IP adres" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adres", + "password": "Wachtwoord" + }, + "description": "Geef het IP adress of mDNS van het apparaat en het bijbehorend wachtwoord", + "title": "Identificeer het apparaat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/nl.json b/homeassistant/components/airthings_ble/translations/nl.json index 1e1e61a3b39853..785b57f8bb8022 100644 --- a/homeassistant/components/airthings_ble/translations/nl.json +++ b/homeassistant/components/airthings_ble/translations/nl.json @@ -5,7 +5,7 @@ "already_in_progress": "Nederlands", "cannot_connect": "Nederlands", "no_devices_found": "Nederlands", - "unknown": "Nederlands" + "unknown": "Onverwachte fout" }, "flow_title": "Nederlands", "step": { diff --git a/homeassistant/components/brother/translations/it.json b/homeassistant/components/brother/translations/it.json index 21fd1bf15aba68..038815ec142393 100644 --- a/homeassistant/components/brother/translations/it.json +++ b/homeassistant/components/brother/translations/it.json @@ -22,7 +22,7 @@ "type": "Tipo di stampante" }, "description": "Vuoi aggiungere la stampante {model} con il numero seriale `{serial_number}` a Home Assistant?", - "title": "Trovata stampante Brother" + "title": "Rilevata stampante Brother" } } } diff --git a/homeassistant/components/dlna_dms/translations/it.json b/homeassistant/components/dlna_dms/translations/it.json index a8c0d50c0cf359..c671bc9b619f4e 100644 --- a/homeassistant/components/dlna_dms/translations/it.json +++ b/homeassistant/components/dlna_dms/translations/it.json @@ -17,7 +17,7 @@ "host": "Host" }, "description": "Seleziona un dispositivo da configurare", - "title": "Dispositivi DLNA DMA rilevati" + "title": "Rilevati dispositivi DLNA DMA" } } } diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index b1c4e12d088637..23727d1510fb8c 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -21,7 +21,7 @@ }, "discovery_confirm": { "description": "Vuoi aggiungere il nodo ESPHome `{name}` a Home Assistant?", - "title": "Trovato nodo ESPHome" + "title": "Rilevato nodo ESPHome" }, "encryption_key": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/it.json b/homeassistant/components/forecast_solar/translations/it.json index 598c67695cc713..8d6d266fa95953 100644 --- a/homeassistant/components/forecast_solar/translations/it.json +++ b/homeassistant/components/forecast_solar/translations/it.json @@ -25,7 +25,7 @@ "inverter_size": "Dimensioni inverter (Watt)", "modules power": "Potenza di picco totale in Watt dei tuoi moduli solari" }, - "description": "Questi valori consentono di modificare il risultato di Solar.Forecast. Fai riferimento alla documentazione se un campo non \u00e8 chiaro." + "description": "Questi valori consentono di modificare il risultato di Forecast.Solar. Se un campo non \u00e8 chiaro, consultare la documentazione." } } } diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index b1fa200731e88e..bb18e38d1ebc34 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -20,7 +20,7 @@ "password": "Password", "username": "Nome utente" }, - "description": "FRITZ! Box rilevato: {name} \n\n Configura gli strumenti del FRITZ! Box per controllare il tuo {name}", + "description": "FRITZ! Box rilevato: {name} \n\nConfigura gli strumenti del FRITZ! Box per controllare il tuo {name}", "title": "Configura gli strumenti del FRITZ!Box" }, "reauth_confirm": { diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 0fd99c649a122d..4e8cdb513cd958 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -77,9 +77,9 @@ "step": { "confirm_still": { "data": { - "confirmed_ok": "Questa immagine appare bene" + "confirmed_ok": "Questa immagine sembra buona." }, - "description": "![Anteprima dell' immagine fissa della fotocamera]({preview_url})", + "description": "![Anteprima immagine fissa fotocamera]({preview_url})", "title": "Anteprima" }, "content_type": { diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 13e928e998ba02..71a0066b58abbc 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -36,6 +36,10 @@ "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 de Docker CGroup incorrecta. Clica l'enlla\u00e7 per con\u00e8ixer la versi\u00f3 correcta i sobre com solucionar-ho.", "title": "Sistema no compatible - Versi\u00f3 de CGroup" }, + "unsupported_dns_server": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 el servidor DNS proporcionat no funciona correctament i la segona opci\u00f3 est\u00e0 desactivada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes el servidor DNS" + }, "unsupported_docker_configuration": { "description": "El sistema no \u00e9s compatible perqu\u00e8 el 'deamon' de Docker est\u00e0 funcionant de manera inesperada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", "title": "Sistema no compatible - Docker mal configurat" diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index be64a3b0624c62..3698677ef8cb02 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -7,7 +7,7 @@ "cannot_connect": "Impossibile connettersi", "discover_timeout": "Impossibile trovare i bridge Hue", "invalid_host": "Host non valido", - "no_bridges": "Nessun bridge di Philips Hue trovato", + "no_bridges": "Nessun bridge di Philips Hue rilevato", "not_hue_bridge": "Non \u00e8 un bridge Hue", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index c5bd0f1af87477..38a04fa1fe0f38 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Impossibile connettersi", - "not_insteon_device": "Dispositivo rilevato non \u00e8 un dispositivo Insteon", + "not_insteon_device": "Il dispositivo rilevato non \u00e8 un dispositivo Insteon", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index 96319641c6b349..3c2df9a6ee05d3 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "Vuoi configurare {name}?", - "title": "Stampante rilevata" + "title": "Rilevata stampante" } } } diff --git a/homeassistant/components/melnor/translations/it.json b/homeassistant/components/melnor/translations/it.json index 9124fdfce13792..c4929c9c230f44 100644 --- a/homeassistant/components/melnor/translations/it.json +++ b/homeassistant/components/melnor/translations/it.json @@ -6,7 +6,7 @@ "step": { "bluetooth_confirm": { "description": "Vuoi aggiungere la valvola Bluetooth Melnor `{name}` a Home Assistant?", - "title": "Scoperta la valvola Bluetooth Melnor" + "title": "Rilevata valvola Bluetooth Melnor" } } } diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index b9504a80a0326b..f8add8256bcd5d 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -8,7 +8,7 @@ "bad_birth": "Argomento di nascita non valido", "bad_certificate": "Il certificato CA non \u00e8 valido", "bad_client_cert": "Certificato client non valido, assicurarsi che venga fornito un file codificato PEM", - "bad_client_cert_key": "Il certificato del client e il privato non sono accoppiati in modo valido", + "bad_client_cert_key": "Il certificato del client e il certificato privato non sono una coppia valida", "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password", "bad_discovery_prefix": "Prefisso di ricerca non valido", "bad_will": "Argomento testamento non valido", @@ -25,13 +25,13 @@ "client_id": "ID cliente (lasciare vuoto per generarne uno in modo casuale)", "client_key": "Percorso per un file della chiave privata", "discovery": "Attiva il rilevamento", - "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento attivo", + "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento attivit\u00e0", "password": "Password", "port": "Porta", "protocol": "Protocollo MQTT", "set_ca_cert": "Convalida del certificato del broker", - "set_client_cert": "Utilizzare un certificato client", - "tls_insecure": "Ignorare la convalida del certificato del broker", + "set_client_cert": "Usa un certificato client", + "tls_insecure": "Ignora la convalida del certificato del broker", "username": "Nome utente" }, "description": "Inserisci le informazioni di connessione del tuo broker MQTT." @@ -80,11 +80,11 @@ "options": { "error": { "bad_birth": "Argomento di nascita non valido", - "bad_certificate": "Certificato CA non valido", + "bad_certificate": "Il certificato CA non \u00e8 valido", "bad_client_cert": "Certificato cliente non valido, assicurarsi che venga fornito un file con codice PEM", - "bad_client_cert_key": "Il certificato del client e il certificato privato non sono accoppiati in modo valido", + "bad_client_cert_key": "Il certificato del client e il certificato privato non sono una coppia valida", "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password", - "bad_discovery_prefix": "Prefisso di ricerca non valido", + "bad_discovery_prefix": "Prefisso di rilevamento non valido", "bad_will": "Argomento di testamento non valido", "cannot_connect": "Impossibile connettersi", "invalid_inclusion": "Il certificato e la chiave privata del client devono essere configurati insieme" @@ -118,7 +118,7 @@ "birth_retain": "Persistenza del messaggio birth", "birth_topic": "Argomento del messaggio birth", "discovery": "Attiva il rilevamento", - "discovery_prefix": "Scopri il prefisso", + "discovery_prefix": "Rileva prefisso", "will_enable": "Abilita il messaggio testamento", "will_payload": "Payload del messaggio testamento", "will_qos": "QoS del messaggio testamento", diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 155cf0bf07aafd..13fcd4a9595c21 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -5,15 +5,30 @@ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "bad_birth": "Ongeldig `birth` topic", + "bad_certificate": "Het CA certificaat bestand is ongeldig", + "bad_client_cert": "Ongeldig client-certificaat, zorg voor een PEM gecodeerd bestand", + "bad_client_cert_key": "Client certificaat en priv\u00e9sleutel zijn geen geldig paar", + "bad_client_key": "Ongeldige priv\u00e9sleutel (private key), zorg voor een PEM gecodeerd bestand zonder wachtwoord", + "bad_discovery_prefix": "Ongeldig discovery voorvoegsel", + "bad_will": "Ongeldig `will` topic", + "cannot_connect": "Kan geen verbinding maken", + "invalid_inclusion": "Het client-certificaat en priv\u00e9sleutel moeten samen worden geconfigureerd" }, "step": { "broker": { "data": { + "advanced_options": "Geavanceerde opties", "broker": "Broker", + "client_id": "Client ID (leeg laten voor een willekeurig ID)", "discovery": "Detectie inschakelen", + "keepalive": "Tijd tussen het verzenden van keep-a-live berichten", "password": "Wachtwoord", "port": "Poort", + "protocol": "MQTT protocol", + "set_ca_cert": "Broker certificaatvalidatie", + "set_client_cert": "Gebruik een client-certificaat", + "tls_insecure": "Negeer validatie van brokercertificaten", "username": "Gebruikersnaam" }, "description": "MQTT" @@ -49,18 +64,44 @@ "button_triple_press": "\" {subtype} \" driemaal geklikt" } }, + "issues": { + "deprecated_yaml": { + "description": "Handmatig geconfigureerd MQTT {platform}(en) gevonden onder de platform sleutel `{platform}`.\n\nVerplaats a.u.b. deze configuratieinstellingen naar de `mqtt` configuratie sleutel en herstart Home Assistant om het probleem te verhelpen. Zie de [documentatie]({more_info_url}), voor meer informatie.", + "title": "Handmatig geconfigureerd platform {platform}(s) vereist aandacht" + }, + "deprecated_yaml_broker_settings": { + "description": "De volgende instellingen gevonden in `configuration.yaml` zijn gemigreerd naar de MQTT configuratieinstellingen en overschrijven nu de instellingen in `configuration.yaml`:\n`{deprecated_settings}`\n\nVerwijderd deze instellingen van `configuration.yaml` en herstart Home Assistant om dit probleem op te lossen. Zie de [documentatie]({more_info_url}), voor meer informatie.", + "title": "Verouderde MQTT instellingen gevonden in `configuration.yaml`" + } + }, "options": { "error": { "bad_birth": "Ongeldig birth topic", + "bad_certificate": "Het CA certificaat bestand is ongeldig", + "bad_client_cert": "Ongeldig client certificaat, zorg voor een PEM gecodeerd bestand", + "bad_client_cert_key": "Client-certificaat en priv\u00e9sleutel zijn geen geldig paar", + "bad_client_key": "Ongeldige priv\u00e9sleutel (private key), zorg voor een PEM gecodeerd bestand zonder wachtwoord", + "bad_discovery_prefix": "Ongeldig discovery voorvoegsel", "bad_will": "Ongeldig will topic", - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_inclusion": "Het client-certificaat en priv\u00e9sleutel moeten samen worden geconfigureerd" }, "step": { "broker": { "data": { + "advanced_options": "Geavanceerde opties", "broker": "Broker", + "certificate": "Upload CA certificaat bestand", + "client_cert": "Upload private certificaat bestand", + "client_id": "Client ID (leeg laten voor een willekeurig ID)", + "client_key": "Upload private key bestand", + "keepalive": "Tijd tussen het verzenden van keep-a-live berichten", "password": "Wachtwoord", "port": "Poort", + "protocol": "MQTT protocol", + "set_ca_cert": "Broker certificaatvalidatie", + "set_client_cert": "Gebruik een client-certificaat", + "tls_insecure": "Negeer validatie van brokercertificaten", "username": "Gebruikersnaam" }, "description": "Voer de verbindingsgegevens van uw MQTT-broker in.", @@ -68,14 +109,15 @@ }, "options": { "data": { - "birth_enable": "Geboortebericht inschakelen", - "birth_payload": "Birth message payload", - "birth_qos": "Birth message QoS", + "birth_enable": "Geboortebericht inschakelen (birth)", + "birth_payload": "Birth bericht inhoud", + "birth_qos": "Birth bericht QoS", "birth_retain": "Verbind bericht onthouden", - "birth_topic": "Birth message onderwerp", + "birth_topic": "Birth bericht topic", "discovery": "Discovery inschakelen", - "will_enable": "Offline bericht inschakelen", - "will_payload": "Offline bericht inhoud", + "discovery_prefix": "Discovery-voorvoegsel", + "will_enable": "Offline bericht inschakelen (will)", + "will_payload": "Will bericht inhoud", "will_qos": "Offline bericht QoS", "will_retain": "Offline bericht onthouden", "will_topic": "Offline bericht topic" diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 44bd06022b990c..51ddd56e6d6040 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -44,9 +44,9 @@ "gw_mqtt": { "data": { "persistence_file": "persistentiebestand (leeg laten om automatisch te genereren)", - "retain": "mqtt behouden", + "retain": "MQTT retentie", "topic_in_prefix": "prefix voor inkomende topics (topic_in_prefix)", - "topic_out_prefix": "prefix voor uitgaande topics (topic_out_prefix)", + "topic_out_prefix": "voorvoegsel voor uitgaande topics (topic_out_prefix)", "version": "MySensors-versie" }, "description": "MQTT-gateway instellen" diff --git a/homeassistant/components/nobo_hub/translations/it.json b/homeassistant/components/nobo_hub/translations/it.json index 2b8100c49e2e58..28bb8f94f342b5 100644 --- a/homeassistant/components/nobo_hub/translations/it.json +++ b/homeassistant/components/nobo_hub/translations/it.json @@ -25,7 +25,7 @@ }, "user": { "data": { - "device": "Hub scoperti" + "device": "Rilevati hub" }, "description": "Seleziona Nob\u00f8 Ecohub da configurare." } diff --git a/homeassistant/components/oralb/translations/it.json b/homeassistant/components/oralb/translations/it.json index b19851b36ee1db..7784ed3a24020d 100644 --- a/homeassistant/components/oralb/translations/it.json +++ b/homeassistant/components/oralb/translations/it.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_in_progress": "Il processo di configurazione \u00e8 gi\u00e0 in corso", - "no_devices_found": "Nessun dispositivo trovato in rete", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", "not_supported": "Dispositivo non supportato" }, - "flow_title": "{nome}", + "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "Vuoi impostare {nome}?" + "description": "Vuoi configurare {name}?" }, "user": { "data": { "address": "Dispositivo" }, - "description": "Scegliere un dispositivo da configurare" + "description": "Seleziona un dispositivo da configurare" } } } diff --git a/homeassistant/components/plugwise/translations/select.it.json b/homeassistant/components/plugwise/translations/select.it.json index 97106b6c5b92dc..3407312874e8ea 100644 --- a/homeassistant/components/plugwise/translations/select.it.json +++ b/homeassistant/components/plugwise/translations/select.it.json @@ -1,10 +1,10 @@ { "state": { "plugwise__dhw_mode": { - "auto": "Automatico", + "auto": "Automatica", "boost": "Velocizza", "comfort": "Comfort", - "off": "Spento" + "off": "Spenta" }, "plugwise__regulation_mode": { "bleeding_cold": "Sfiatamento freddo", diff --git a/homeassistant/components/pure_energie/translations/it.json b/homeassistant/components/pure_energie/translations/it.json index f3b7419fc1d896..ebef5a12c99add 100644 --- a/homeassistant/components/pure_energie/translations/it.json +++ b/homeassistant/components/pure_energie/translations/it.json @@ -19,7 +19,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere Pure Energie Meter (`{model}`) a Home Assistant?", - "title": "Scoperto dispositivo Pure Energie Meter" + "title": "Rilevato dispositivo Pure Energie Meter" } } } diff --git a/homeassistant/components/pushover/translations/it.json b/homeassistant/components/pushover/translations/it.json index b42f4e62b77d3a..1fe83629c684ab 100644 --- a/homeassistant/components/pushover/translations/it.json +++ b/homeassistant/components/pushover/translations/it.json @@ -32,7 +32,7 @@ }, "removed_yaml": { "description": "La configurazione di Pushover tramite YAML \u00e8 stata rimossa.\n\nLa configurazione YAML esistente non sar\u00e0 utilizzata da Home Assistant.\n\nRimuovere la configurazione YAML di Pushover dal file configuration.yaml e riavviare Home Assistant per risolvere il problema.", - "title": "La configurazione Pushover YAML \u00e8 stata rimossa" + "title": "La configurazione YAML di Pushover \u00e8 stata rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.it.json b/homeassistant/components/sensibo/translations/sensor.it.json index cb611c4cf42edb..1da3b54d850c4a 100644 --- a/homeassistant/components/sensibo/translations/sensor.it.json +++ b/homeassistant/components/sensibo/translations/sensor.it.json @@ -5,7 +5,7 @@ "s": "Sensibile" }, "sensibo__smart_type": { - "feelslike": "Sembra che", + "feelslike": "Percepita", "humidity": "Umidit\u00e0", "temperature": "Temperatura" } diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index 7c18d944ab77dd..7b3a0472f41e25 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere il dispositivo Smappee con numero di serie `{serialnumber}` a Home Assistant?", - "title": "Dispositivo Smappee rilevato" + "title": "Rilevato dispositivo Smappee" } } } diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json index 36227a1e9bce99..2025bf2f336563 100644 --- a/homeassistant/components/wled/translations/it.json +++ b/homeassistant/components/wled/translations/it.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere il WLED chiamato `{name}` a Home Assistant?", - "title": "Dispositivo WLED rilevato" + "title": "Rilevato dispositivo WLED" } } }, diff --git a/homeassistant/components/xiaomi_ble/translations/nl.json b/homeassistant/components/xiaomi_ble/translations/nl.json index 6b79e0311debbc..2378ad0d84be3d 100644 --- a/homeassistant/components/xiaomi_ble/translations/nl.json +++ b/homeassistant/components/xiaomi_ble/translations/nl.json @@ -3,14 +3,34 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", + "decryption_failed": "De opgegeven `bindkey` werkte niet, de sensorgegevens konden niet worden ontsleuteld. Controleer dit en probeer het opnieuw.", + "expected_24_characters": "Verwachtte een hexadecimale `bindkey` van 24 karakters.", + "expected_32_characters": "Verwachtte een hexadecimale `bindkey` van 32 karakters.", "no_devices_found": "Geen apparaten gevonden op het netwerk", "reauth_successful": "Herauthenticatie geslaagd" }, + "error": { + "decryption_failed": "De opgegeven `bindkey` werkte niet, de sensorgegevens konden niet worden ontsleuteld. Controleer dit en probeer het opnieuw.", + "expected_24_characters": "Verwachtte een hexadecimale `bindkey` van 24 karakters.", + "expected_32_characters": "Verwachtte een hexadecimale `bindkey` van 32 karakters." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Wilt u {name} instellen?" }, + "confirm_slow": { + "description": "Er is de laatste minuut geen aankondigingsbericht van dit apparaat ontvangen, dus we weten niet zeker of dit apparaat encryptie gebruikt of niet. Dit kan zijn omdat het apparaat een trage aankodigings interval heeft. Bevestig om dit apparaat hoe dan ook toe te voegen, dan zal wanneer binnenkort een aankodiging wordt ontvangen worden gevraagd om de `bindkey` als dat nodig is." + }, + "get_encryption_key_4_5": { + "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 32 tekens nodig." + }, + "get_encryption_key_legacy": { + "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 24 tekens nodig." + }, + "slow_confirm": { + "description": "Er is de laatste minuut geen aankondigingsbericht van dit apparaat ontvangen, dus we weten niet zeker of dit apparaat encryptie gebruikt of niet. Dit kan zijn omdat het apparaat een trage aankodigings interval heeft. Bevestig om dit apparaat hoe dan ook toe te voegen, dan zal wanneer binnenkort een aankodiging wordt ontvangen worden gevraagd om de `bindkey` als dat nodig is." + }, "user": { "data": { "address": "Apparaat" diff --git a/homeassistant/components/xiaomi_miio/translations/select.nl.json b/homeassistant/components/xiaomi_miio/translations/select.nl.json index 8041e47ab3e6b2..71ffb1a02bec9f 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.nl.json +++ b/homeassistant/components/xiaomi_miio/translations/select.nl.json @@ -1,12 +1,13 @@ { "state": { "xiaomi_miio__display_orientation": { + "forward": "Vooruit", "left": "Links", "right": "Rechts" }, "xiaomi_miio__led_brightness": { "bright": "Helder", - "dim": "Dim", + "dim": "Dimmen", "off": "Uit" }, "xiaomi_miio__ptc_level": { diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index e408a9949bc9cb..f7bb65db547e08 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -55,6 +55,7 @@ "data": { "uploaded_backup_file": "Een bestand uploaden" }, + "description": "Herstel je netwerkinstellingen van ge-upload backup-JSON-bestand. Je kunt deze dpwnloaden van een andere ZHA installatie via **Netwerkinstellingen**, of gebruik een Zigbee2MQTT `coordinator_backup.json` bestand.", "title": "Upload een handmatige back-up" }, "user": { @@ -174,6 +175,7 @@ "data": { "uploaded_backup_file": "Een bestand uploaden" }, + "description": "Herstel je netwerkinstellingen van ge-upload backup-JSON-bestand. Je kunt deze dpwnloaden van een andere ZHA installatie via **Netwerkinstellingen**, of gebruik een Zigbee2MQTT `coordinator_backup.json` bestand.", "title": "Upload een handmatige back-up" } } diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index d104f510eaf747..d43771349775c2 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -62,7 +62,7 @@ }, "zeroconf_confirm": { "description": "Vuoi aggiungere il server Z-Wave JS con l'ID casa {home_id} trovato in {url} a Home Assistant?", - "title": "Server JS Z-Wave rilevato" + "title": "Rilevato Server JS Z-Wave" } } }, From 8e965eb56f8504fcacefc20f1fa298897dfb40dc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sun, 6 Nov 2022 10:21:48 +0100 Subject: [PATCH 244/394] Bump pyatmo to 7.4.0 (#81636) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 436b6329c1da65..ac478282614258 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==7.3.0"], + "requirements": ["pyatmo==7.4.0"], "after_dependencies": ["cloud", "media_source"], "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], diff --git a/requirements_all.txt b/requirements_all.txt index f54c88aad8ea57..7b48b82d6283ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1448,7 +1448,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.4.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7793088d47cf5f..a06d89a41e0fd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1036,7 +1036,7 @@ pyalmond==0.0.2 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==7.3.0 +pyatmo==7.4.0 # homeassistant.components.apple_tv pyatv==0.10.3 From cdec4fe110a3a05afdda911e72e466c5902ac821 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 05:38:01 -0600 Subject: [PATCH 245/394] Bump oralb-ble to 0.13.0 (#81622) * Bump oralb-ble to 0.11.1 adds some more missing pressure mappings changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.10.2...v0.11.1 * bump again to update for additional reports * bump again for more data from issue reports --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index ba89c73a240fef..1738558770e86b 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.10.2"], + "requirements": ["oralb-ble==0.13.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 7b48b82d6283ab..c5bccac4cc0f38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1244,7 +1244,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.10.2 +oralb-ble==0.13.0 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a06d89a41e0fd6..ed13813fdf5c69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -889,7 +889,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.10.2 +oralb-ble==0.13.0 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 4c65a2f455fe689814266597190bbc8270419dc7 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 6 Nov 2022 13:02:59 +0100 Subject: [PATCH 246/394] Bump PyXiaomiGateway to 0.14.3 (#81603) Fixes: #80249 --- homeassistant/components/xiaomi_aqara/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index a70fb90f961cad..8152a77a73e900 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi Gateway (Aqara)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", - "requirements": ["PyXiaomiGateway==0.14.1"], + "requirements": ["PyXiaomiGateway==0.14.3"], "after_dependencies": ["discovery"], "codeowners": ["@danielhiversen", "@syssi"], "zeroconf": ["_miio._udp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index c5bccac4cc0f38..4cff3722acc851 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,7 @@ PyTurboJPEG==1.6.7 PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.14.1 +PyXiaomiGateway==0.14.3 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed13813fdf5c69..94ded26c853c35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -46,7 +46,7 @@ PyTurboJPEG==1.6.7 PyViCare==2.17.0 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.14.1 +PyXiaomiGateway==0.14.3 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From 4056f673b83c7cef774e72efee1f4c2f8668ac16 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sun, 6 Nov 2022 14:51:19 +0100 Subject: [PATCH 247/394] Fix watermeter issue for old P1 Monitor versions (#81570) * Bump the python package version * Add exception to check if user has a water meter --- homeassistant/components/p1_monitor/__init__.py | 5 +++-- homeassistant/components/p1_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index b157f3e811665d..e6178ffeb4106c 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -5,6 +5,7 @@ from p1monitor import ( P1Monitor, + P1MonitorConnectionError, P1MonitorNoDataError, Phases, Settings, @@ -101,8 +102,8 @@ async def _async_update_data(self) -> P1MonitorData: try: data[SERVICE_WATERMETER] = await self.p1monitor.watermeter() self.has_water_meter = True - except P1MonitorNoDataError: - LOGGER.debug("No watermeter data received from P1 Monitor") + except (P1MonitorNoDataError, P1MonitorConnectionError): + LOGGER.debug("No water meter data received from P1 Monitor") if self.has_water_meter is None: self.has_water_meter = False diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index 626cff15dfae88..2e699276caa7fa 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "P1 Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/p1_monitor", - "requirements": ["p1monitor==2.1.0"], + "requirements": ["p1monitor==2.1.1"], "codeowners": ["@klaasnicolaas"], "quality_scale": "platinum", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 4cff3722acc851..612464228530df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1256,7 +1256,7 @@ orvibo==1.1.1 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==2.1.0 +p1monitor==2.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94ded26c853c35..5a7285ced42be1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -895,7 +895,7 @@ oralb-ble==0.13.0 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==2.1.0 +p1monitor==2.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr From b18a1e6d7c1c89375659cfd39e99e26e42d23bf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 08:12:58 -0600 Subject: [PATCH 248/394] Bump dbus-fast to 1.72.0 (#81574) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0b94e1bdfd9e15..a0a9629c9f406a 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,7 +10,7 @@ "bleak-retry-connector==2.8.2", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", - "dbus-fast==1.71.0" + "dbus-fast==1.72.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eb580796a97730..f6b700be209efe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.3 -dbus-fast==1.71.0 +dbus-fast==1.72.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 612464228530df..71e3e4725a252c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -546,7 +546,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.71.0 +dbus-fast==1.72.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a7285ced42be1..87a427efa45e32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.71.0 +dbus-fast==1.72.0 # homeassistant.components.debugpy debugpy==1.6.3 From 6c659c0d68174a516ae09a833bf9fa98074d0d50 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 6 Nov 2022 10:11:49 -0500 Subject: [PATCH 249/394] Add repair warning about UniFi Protect Early Access (#81658) --- .../components/unifiprotect/__init__.py | 17 ++++++++++++++++- .../components/unifiprotect/strings.json | 6 ++++++ .../unifiprotect/translations/en.json | 10 ++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 30b1d1ad56d970..649d62c6feee3d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -21,8 +21,9 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.issue_registry import IssueSeverity from .const import ( CONF_ALL_UPDATES, @@ -101,6 +102,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) + protect_version = data_service.api.bootstrap.nvr.version + if protect_version.is_prerelease: + ir.async_create_issue( + hass, + DOMAIN, + f"ea_warning_{protect_version}", + is_fixable=False, + is_persistent=False, + learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", + severity=IssueSeverity.WARNING, + translation_key="ea_warning", + translation_placeholders={"version": str(protect_version)}, + ) + return True diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d3cfe24abd2a83..2967ba8c82eb9e 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -54,5 +54,11 @@ } } } + }, + "issues": { + "ea_warning": { + "title": "{version} is an Early Access version of UniFi Protect", + "description": "You are using {version} of UniFi Protect. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected." + } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index c6050d052848c1..9fd074ac585cd7 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -41,16 +41,18 @@ } } }, + "issues": { + "ea_warning": { + "description": "You are using {version} of UniFi Protect. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected.", + "title": "{version} is an Early Access version of UniFi Protect" + } + }, "options": { - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" - }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, From 3630de909cc311912cee44e1c450506d71ffa1cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 09:47:07 -0600 Subject: [PATCH 250/394] Bump aiohomekit to 2.2.17 (#81657) Improve BLE pairing reliability, especially with esp32 proxies changelog: https://github.com/Jc2k/aiohomekit/compare/2.2.16...2.2.17 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index e4a2b5d79bb839..7a088993e9f152 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.16"], + "requirements": ["aiohomekit==2.2.17"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 71e3e4725a252c..0ac216fc7dbf33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,7 +174,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.16 +aiohomekit==2.2.17 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87a427efa45e32..fdb7b49514ba2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.16 +aiohomekit==2.2.17 # homeassistant.components.emulated_hue # homeassistant.components.http From d3529cb346b2d55a7b12d6a5c4cd57cdd24d593e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 12:59:32 -0600 Subject: [PATCH 251/394] Add missing h2 dep to iaqualink (#81630) fixes https://github.com/home-assistant/core/issues/81439 --- homeassistant/components/iaqualink/manifest.json | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index d5b7d7de0d8bf5..f274cd5ea1ccb4 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.5.0"], + "requirements": ["iaqualink==0.5.0", "h2==4.1.0"], "iot_class": "cloud_polling", "loggers": ["iaqualink"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0ac216fc7dbf33..9db4423df2de23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -821,6 +821,9 @@ gstreamer-player==1.1.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.iaqualink +h2==4.1.0 + # homeassistant.components.homekit ha-HAP-python==4.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdb7b49514ba2f..80e2f39ba74c1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -613,6 +613,9 @@ gspread==5.5.0 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.iaqualink +h2==4.1.0 + # homeassistant.components.homekit ha-HAP-python==4.5.2 From 496f78bae5f475dfc44c571893d9a612ae0b63a1 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sun, 6 Nov 2022 21:37:44 +0200 Subject: [PATCH 252/394] FIX: patch correct async_setup_entry in tilt_ble (#81671) --- tests/components/tilt_ble/test_config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/tilt_ble/test_config_flow.py b/tests/components/tilt_ble/test_config_flow.py index c2bb20e0dd6c6e..789bb86e30c6e7 100644 --- a/tests/components/tilt_ble/test_config_flow.py +++ b/tests/components/tilt_ble/test_config_flow.py @@ -21,7 +21,7 @@ async def test_async_step_bluetooth_valid_device(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "bluetooth_confirm" with patch( - "homeassistant.components.govee_ble.async_setup_entry", return_value=True + "homeassistant.components.tilt_ble.async_setup_entry", return_value=True ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} From d62bac9c5948cb7d9c89cb25ac8a0cd1d63906e0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 6 Nov 2022 12:38:55 -0700 Subject: [PATCH 253/394] Fix missing RainMachine restrictions switches (#81673) --- homeassistant/components/rainmachine/switch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 35b0e5eab2b584..66081588f27e4f 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -241,6 +241,7 @@ async def async_setup_entry( # Add switches to control restrictions: for description in RESTRICTIONS_SWITCH_DESCRIPTIONS: + coordinator = data.coordinators[description.api_category] if not key_exists(coordinator.data, description.data_key): continue entities.append(RainMachineRestrictionSwitch(entry, data, description)) From df7000f96d5c3f7a01b545eb19c33eb9d9367d0f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 6 Nov 2022 21:23:48 +0100 Subject: [PATCH 254/394] Always use Celsius in Shelly integration, part 2 (#81602) * Always use Celsius in Shelly integration * Update homeassistant/components/shelly/sensor.py Co-authored-by: Aarni Koskela * Restore unit from the registry during HA startup Co-authored-by: Aarni Koskela --- homeassistant/components/shelly/entity.py | 5 ++++ homeassistant/components/shelly/sensor.py | 30 +++++++++++++++-------- homeassistant/components/shelly/utils.py | 11 ++------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index fd92ea41408393..96f566f6a2e76c 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -9,6 +9,7 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry, entity, entity_registry @@ -615,6 +616,7 @@ def __init__( """Initialize the sleeping sensor.""" self.sensors = sensors self.last_state: StateType = None + self.last_unit: str | None = None self.coordinator = coordinator self.attribute = attribute self.block: Block | None = block # type: ignore[assignment] @@ -644,6 +646,7 @@ async def async_added_to_hass(self) -> None: if last_state is not None: self.last_state = last_state.state + self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @callback def _update_callback(self) -> None: @@ -696,6 +699,7 @@ def __init__( ) -> None: """Initialize the sleeping sensor.""" self.last_state: StateType = None + self.last_unit: str | None = None self.coordinator = coordinator self.key = key self.attribute = attribute @@ -725,3 +729,4 @@ async def async_added_to_hass(self) -> None: if last_state is not None: self.last_state = last_state.state + self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index cf1eb7508c7de5..922a5ac8b5c234 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -47,7 +47,7 @@ async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import get_device_entry_gen, get_device_uptime, temperature_unit +from .utils import get_device_entry_gen, get_device_uptime @dataclass @@ -79,7 +79,7 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): ("device", "deviceTemp"): BlockSensorDescription( key="device|deviceTemp", name="Device Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -221,7 +221,7 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): ("sensor", "temp"): BlockSensorDescription( key="sensor|temp", name="Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -230,7 +230,7 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): ("sensor", "extTemp"): BlockSensorDescription( key="sensor|extTemp", name="Temperature", - unit_fn=temperature_unit, + native_unit_of_measurement=TEMP_CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -499,8 +499,6 @@ def __init__( super().__init__(coordinator, block, attribute, description) self._attr_native_unit_of_measurement = description.native_unit_of_measurement - if unit_fn := description.unit_fn: - self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) @property def native_value(self) -> StateType: @@ -547,10 +545,6 @@ def __init__( """Initialize the sleeping sensor.""" super().__init__(coordinator, block, attribute, description, entry, sensors) - self._attr_native_unit_of_measurement = description.native_unit_of_measurement - if block and (unit_fn := description.unit_fn): - self._attr_native_unit_of_measurement = unit_fn(block.info(attribute)) - @property def native_value(self) -> StateType: """Return value of sensor.""" @@ -559,6 +553,14 @@ def native_value(self) -> StateType: return self.last_state + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if self.block is not None: + return self.entity_description.native_unit_of_measurement + + return self.last_unit + class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity): """Represent a RPC sleeping sensor.""" @@ -572,3 +574,11 @@ def native_value(self) -> StateType: return self.attribute_value return self.last_state + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor, if any.""" + if self.coordinator.device.initialized: + return self.entity_description.native_unit_of_measurement + + return self.last_unit diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index a13d84d32bea06..bf242d47e6cbd8 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -5,13 +5,13 @@ from typing import Any, cast from aiohttp.web import Request, WebSocketResponse -from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice +from aioshelly.block_device import COAP, Block, BlockDevice from aioshelly.const import MODEL_NAMES from aioshelly.rpc_device import RpcDevice, WsServer from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry, entity_registry, singleton from homeassistant.helpers.typing import EventType @@ -43,13 +43,6 @@ def async_remove_shelly_entity( entity_reg.async_remove(entity_id) -def temperature_unit(block_info: dict[str, Any]) -> str: - """Detect temperature unit.""" - if block_info[BLOCK_VALUE_UNIT] == "F": - return TEMP_FAHRENHEIT - return TEMP_CELSIUS - - def get_block_device_name(device: BlockDevice) -> str: """Naming for device.""" return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) From 04d01cefb24b27af1039ba2adc8889dfa0943cd9 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Sun, 6 Nov 2022 23:26:40 +0300 Subject: [PATCH 255/394] Fix Bravia TV options flow when device is off (#81644) * Fix options flow when tv is off * abort with message --- homeassistant/components/braviatv/config_flow.py | 6 +++++- homeassistant/components/braviatv/strings.json | 3 +++ homeassistant/components/braviatv/translations/en.json | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index f5c7826b82547f..45a2bad00361b1 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -262,7 +262,11 @@ async def async_step_init( self.config_entry.entry_id ] - await coordinator.async_update_sources() + try: + await coordinator.async_update_sources() + except BraviaTVError: + return self.async_abort(reason="failed_update") + sources = coordinator.source_map.values() self.source_list = [item["title"] for item in sources] return await self.async_step_user() diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index 4dd08135896e9a..ac651955166da7 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -48,6 +48,9 @@ "ignored_sources": "List of ignored sources" } } + }, + "abort": { + "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." } } } diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index c3341d33112805..39c95c706debe8 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." + }, "step": { "user": { "data": { From e7b5aaec47feb1ebb167c5ad73a313e4225f9608 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 17:13:14 -0600 Subject: [PATCH 256/394] Bump aioesphomeapi to 11.4.3 (#81676) --- 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 64cd6b4029c6aa..2070b8ae362052 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==11.4.2"], + "requirements": ["aioesphomeapi==11.4.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/requirements_all.txt b/requirements_all.txt index 9db4423df2de23..eecb2b498e62a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -156,7 +156,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.2 +aioesphomeapi==11.4.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80e2f39ba74c1e..375c30ff5e137f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -143,7 +143,7 @@ aioecowitt==2022.09.3 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==11.4.2 +aioesphomeapi==11.4.3 # homeassistant.components.flo aioflo==2021.11.0 From c3946159d8f6d639e3d3dfbb64dad72feda5fd57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 17:45:53 -0600 Subject: [PATCH 257/394] Bump bleak-retry-connector to 2.8.3 (#81675) Improves chances of making a BLE connection with the ESP32s changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.2...v2.8.3 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index a0a9629c9f406a..fbf9276c9eb871 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.1", - "bleak-retry-connector==2.8.2", + "bleak-retry-connector==2.8.3", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "dbus-fast==1.72.0" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f6b700be209efe..14a4f31f5210ad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 bleak==0.19.1 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index eecb2b498e62a6..dd99cec3e3aa88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth bleak==0.19.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 375c30ff5e137f..79d8224df051dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.2 +bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth bleak==0.19.1 From 3cfcb93d706d407ce2bd98a2dc58bd5681d7c914 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 18:04:45 -0600 Subject: [PATCH 258/394] Bump aiohomekit to 2.2.18 (#81693) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 7a088993e9f152..b18f35390b7985 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.17"], + "requirements": ["aiohomekit==2.2.18"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index dd99cec3e3aa88..58c86250c422b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,7 +174,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.17 +aiohomekit==2.2.18 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79d8224df051dc..19c0777a59f542 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.17 +aiohomekit==2.2.18 # homeassistant.components.emulated_hue # homeassistant.components.http From 2a42a58ec4ea26802c637fc80d00999989bdf8cd Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 7 Nov 2022 01:16:58 +0100 Subject: [PATCH 259/394] Restore negative values for shelly power factors (#81689) fixes undefined --- homeassistant/components/shelly/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 922a5ac8b5c234..7d99f015c5e92b 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -140,7 +140,7 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): key="emeter|powerFactor", name="Power Factor", native_unit_of_measurement=PERCENTAGE, - value=lambda value: abs(round(value * 100, 1)), + value=lambda value: round(value * 100, 1), device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), From e2788f83210bce44d73036f331c17e9d102c02c8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 7 Nov 2022 00:26:17 +0000 Subject: [PATCH 260/394] [ci skip] Translation update --- .../components/airq/translations/ja.json | 20 +++++++++++++++++++ .../components/auth/translations/bg.json | 2 +- .../forecast_solar/translations/ca.json | 2 +- .../google_sheets/translations/nl.json | 6 ++++++ .../components/hassio/translations/ca.json | 16 +++++++++++++-- .../components/netatmo/translations/it.json | 2 +- .../components/scrape/translations/ca.json | 1 + .../unifiprotect/translations/ca.json | 6 ++++++ .../unifiprotect/translations/en.json | 4 ++++ .../unifiprotect/translations/et.json | 6 ++++++ .../unifiprotect/translations/it.json | 6 ++++++ .../unifiprotect/translations/ru.json | 6 ++++++ 12 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/airq/translations/ja.json diff --git a/homeassistant/components/airq/translations/ja.json b/homeassistant/components/airq/translations/ja.json new file mode 100644 index 00000000000000..8bbc8791ddebe9 --- /dev/null +++ b/homeassistant/components/airq/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_input": "\u7121\u52b9\u306a\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9" + }, + "step": { + "user": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/bg.json b/homeassistant/components/auth/translations/bg.json index d07e20a854cc70..4ccfc0ae79bce8 100644 --- a/homeassistant/components/auth/translations/bg.json +++ b/homeassistant/components/auth/translations/bg.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\u0417\u0430 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u0432\u043e-\u0431\u0430\u0437\u0438\u0440\u0430\u043d\u0438 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0438 \u043f\u0430\u0440\u043e\u043b\u0438, \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u0439\u0442\u0435 QR \u043a\u043e\u0434\u0430 \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0410\u043a\u043e \u043d\u044f\u043c\u0430\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0412\u0438 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 \u0438\u043b\u0438 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u0438\u043b\u0438 [Authy](https://authy.com/).\n\n{qr_code}\n\n\u0421\u043b\u0435\u0434 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u0430, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 6-\u0442\u0435 \u0446\u0438\u0444\u0440\u0438 \u043e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0437\u0430 \u0434\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0438\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435\u0442\u043e. \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 QR \u043a\u043e\u0434\u0430, \u043d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u043a\u043e\u0434 **`{code}`**.", + "description": "\u0417\u0430 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u0432\u043e-\u0431\u0430\u0437\u0438\u0440\u0430\u043d\u0438 \u0435\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u0438 \u043f\u0430\u0440\u043e\u043b\u0438, \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u0439\u0442\u0435 QR \u043a\u043e\u0434\u0430 \u0441 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0442\u043e\u0440\u0430. \u0410\u043a\u043e \u043d\u044f\u043c\u0430\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0412\u0438 \u043f\u0440\u0435\u043f\u043e\u0440\u044a\u0447\u0432\u0430\u043c\u0435 [Google Authenticator](https://support.google.com/accounts/answer/1066447) \u0438\u043b\u0438 [Authy](https://authy.com/).\n\n{qr_code}\n\n\u0421\u043b\u0435\u0434 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u0430, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 6-\u0442\u0435 \u0446\u0438\u0444\u0440\u0438 \u043e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0437\u0430 \u0434\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0438\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435\u0442\u043e. \u0410\u043a\u043e \u0438\u043c\u0430\u0442\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 QR \u043a\u043e\u0434\u0430, \u043d\u0430\u043f\u0440\u0430\u0432\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u043a\u043e\u0434 **`{code}`**.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0447\u0440\u0435\u0437 TOTP" } }, diff --git a/homeassistant/components/forecast_solar/translations/ca.json b/homeassistant/components/forecast_solar/translations/ca.json index 141ad11c165564..b2561bb0a79166 100644 --- a/homeassistant/components/forecast_solar/translations/ca.json +++ b/homeassistant/components/forecast_solar/translations/ca.json @@ -25,7 +25,7 @@ "inverter_size": "Pot\u00e8ncia de l'inversor (Watts)", "modules power": "Pot\u00e8ncia m\u00e0xima total dels panells solars" }, - "description": "Aquests valors permeten ajustar els resultats de Solar.Forecast. Consulta la documentaci\u00f3 si tens dubtes sobre algun camp." + "description": "Aquests valors permeten ajustar els resultats de Forecast.Solar. Consulta la documentaci\u00f3 si tens dubtes sobre algun camp." } } } diff --git a/homeassistant/components/google_sheets/translations/nl.json b/homeassistant/components/google_sheets/translations/nl.json index d530b4e3add8de..884233629ea427 100644 --- a/homeassistant/components/google_sheets/translations/nl.json +++ b/homeassistant/components/google_sheets/translations/nl.json @@ -4,13 +4,18 @@ "already_configured": "Account is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", + "create_spreadsheet_failure": "Fout bij het openen van een nieuw werkblad, zie de error log voor details", "invalid_access_token": "Ongeldig toegangstoken", "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", "oauth_error": "Ongeldige tokengegevens ontvangen.", + "open_spreadsheet_failure": "Fout bij het openen van een nieuw werkblad, zie de error log voor details", "reauth_successful": "Herauthenticatie geslaagd", "timeout_connect": "Time-out bij het maken van verbinding", "unknown": "Onverwachte fout" }, + "create_entry": { + "default": "Succesvol aangemeld en werkblad gemaakt op: {url}" + }, "step": { "auth": { "title": "Google-account koppelen" @@ -19,6 +24,7 @@ "title": "Kies een authenticatie methode" }, "reauth_confirm": { + "description": "De Google Sheets integratie vereist dat je opnieuw inlogt met je account", "title": "Integratie herauthenticeren" } } diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 71a0066b58abbc..9b10bbc55e5924 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -1,7 +1,7 @@ { "issues": { "unhealthy": { - "description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.", + "description": "El sistema no \u00e9s saludable a causa de {reason}. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 i per saber com solucionar-ho.", "title": "Sistema no saludable - {reason}" }, "unhealthy_docker": { @@ -25,7 +25,7 @@ "title": "Sistema no saludable - Codi no fiable" }, "unsupported": { - "description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.", + "description": "El sistema no \u00e9s compatible a causa de {reason}. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 i per saber com solucionar-ho.", "title": "Sistema no compatible - {reason}" }, "unsupported_apparmor": { @@ -36,6 +36,18 @@ "description": "El sistema no \u00e9s compatible perqu\u00e8 s'utilitza una versi\u00f3 de Docker CGroup incorrecta. Clica l'enlla\u00e7 per con\u00e8ixer la versi\u00f3 correcta i sobre com solucionar-ho.", "title": "Sistema no compatible - Versi\u00f3 de CGroup" }, + "unsupported_connectivity_check": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Home Assistant no pot determinar quan hi ha connexi\u00f3 a internet disponible. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Comprovaci\u00f3 de connectivitat desactivada" + }, + "unsupported_content_trust": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 Home Assistant no pot verificar que els continguts en execuci\u00f3 s\u00f3n fiables i no han estat modificats per cap atacant. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Comprovaci\u00f3 de continguts fiables desactivada" + }, + "unsupported_dbus": { + "description": "El sistema no \u00e9s compatible perqu\u00e8 D-Bus no est\u00e0 funcionant correctament. Les comunicacions entre el Supervisor i l'amfitri\u00f3 no poden funcionar sense D-Bus. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", + "title": "Sistema no compatible - Problemes amb D-Bus" + }, "unsupported_dns_server": { "description": "El sistema no \u00e9s compatible perqu\u00e8 el servidor DNS proporcionat no funciona correctament i la segona opci\u00f3 est\u00e0 desactivada. Clica l'enlla\u00e7 per a m\u00e9s informaci\u00f3 sobre com solucionar-ho.", "title": "Sistema no compatible - Problemes el servidor DNS" diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index b2210a4375c65a..4511296efa59e1 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -24,7 +24,7 @@ "trigger_subtype": { "away": "Fuori casa", "hg": "protezione antigelo", - "schedule": "programma" + "schedule": "calendarizzazione" }, "trigger_type": { "alarm_started": "{entity_name} ha rilevato un allarme", diff --git a/homeassistant/components/scrape/translations/ca.json b/homeassistant/components/scrape/translations/ca.json index 2503602ab332f0..8998a7a9c693b4 100644 --- a/homeassistant/components/scrape/translations/ca.json +++ b/homeassistant/components/scrape/translations/ca.json @@ -38,6 +38,7 @@ }, "issues": { "moved_yaml": { + "description": "La configuraci\u00f3 de Scrape mitjan\u00e7ant YAML s'ha mogut a una integraci\u00f3.\n\nLa configuraci\u00f3 YAML actual continuar\u00e0 funcionant en les dues pr\u00f2ximes versions de Home Assistant.\n\nMigra la teva configuraci\u00f3 YAML a la nova integraci\u00f3, consulta la documentaci\u00f3 per saber com fer-ho.", "title": "La configuraci\u00f3 YAML de Scrape s'ha eliminat" } }, diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 1d43956510a0cf..7532b2d9e60c15 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Est\u00e0s utilitzant la {version} d'UniFi Protect. Les versions d'acc\u00e9s anticipat no s\u00f3n compatibles amb Home Assistant i poden fer que la teva integraci\u00f3 d'UniFi Protect s'espatlli o no funcioni correctament.", + "title": "{version} \u00e9s una versi\u00f3 d'acc\u00e9s anticipat d'UniFi Protect" + } + }, "options": { "error": { "invalid_mac_list": "Ha de ser una llista d'adreces MAC separades per comes" diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 9fd074ac585cd7..3b6e74db341d28 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -48,11 +48,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" + }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", + "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index 4bd402fa1c2103..9ddf72f925d352 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Kasutad UniFi Protecti {version}. Home Assistant ei toeta varajase juurdep\u00e4\u00e4su versioone ja see v\u00f5ib p\u00f5hjustada UniFi Protecti sidumise katkemist v\u00f5i ei t\u00f6\u00f6ta see ootusp\u00e4raselt.", + "title": "{version} on UniFi Protecti varajase juurdep\u00e4\u00e4su versioon" + } + }, "options": { "error": { "invalid_mac_list": "Peab olema komadega eraldatud MAC-aadresside loend" diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 712df7b7f2a221..d44b3fccf31622 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Stai utilizzando {version} di UniFi Protect. Le versioni di accesso anticipato non sono supportate da Home Assistant e potrebbero causare l'interruzione dell'integrazione di UniFi Protect o non funzionare come previsto.", + "title": "{version} \u00e8 una versione di accesso anticipato di UniFi Protect" + } + }, "options": { "error": { "invalid_mac_list": "Deve essere un elenco di indirizzi MAC separati da virgole" diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index c1e5558c204eeb..2e8e0687f0a8bc 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 UniFi Protect {version}. \u0412\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f Home Assistant \u0438 \u043c\u043e\u0433\u0443\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", + "title": "UniFi Protect {version} \u2014 \u044d\u0442\u043e \u0432\u0435\u0440\u0441\u0438\u044f \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + } + }, "options": { "error": { "invalid_mac_list": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u043e\u0439 \u0441\u043f\u0438\u0441\u043e\u043a MAC-\u0430\u0434\u0440\u0435\u0441\u043e\u0432, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0445 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438." From 9c3bd22e7746a681d1d564fabbd6c66eeb273614 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Nov 2022 19:07:21 -0600 Subject: [PATCH 261/394] Bump bleak to 0.19.2 (#81688) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index fbf9276c9eb871..923ab248a2da44 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,7 +6,7 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.19.1", + "bleak==0.19.2", "bleak-retry-connector==2.8.3", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 14a4f31f5210ad..2aa2179a890751 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 bleak-retry-connector==2.8.3 -bleak==0.19.1 +bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index 58c86250c422b9..bdebb6f8652623 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ bizkaibus==0.1.1 bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth -bleak==0.19.1 +bleak==0.19.2 # homeassistant.components.blebox blebox_uniapi==2.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19c0777a59f542..c05a57e8508b44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ bimmer_connected==0.10.4 bleak-retry-connector==2.8.3 # homeassistant.components.bluetooth -bleak==0.19.1 +bleak==0.19.2 # homeassistant.components.blebox blebox_uniapi==2.1.3 From 499839c5969971ec8ca172d29bf5f21af77e775d Mon Sep 17 00:00:00 2001 From: tstabrawa <59430211+tstabrawa@users.noreply.github.com> Date: Sun, 6 Nov 2022 19:16:22 -0600 Subject: [PATCH 262/394] Fix nuheat temporary hold time (#81635) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 ++ homeassistant/components/nuheat/climate.py | 30 ++----------------- homeassistant/components/nuheat/const.py | 7 ----- homeassistant/components/nuheat/manifest.json | 4 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nuheat/test_climate.py | 2 +- 7 files changed, 10 insertions(+), 39 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index cbff4badc9fa54..d0e4fc993ad477 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -780,6 +780,8 @@ build.json @home-assistant/supervisor /tests/components/nsw_fuel_station/ @nickw444 /homeassistant/components/nsw_rural_fire_service_feed/ @exxamalte /tests/components/nsw_rural_fire_service_feed/ @exxamalte +/homeassistant/components/nuheat/ @tstabrawa +/tests/components/nuheat/ @tstabrawa /homeassistant/components/nuki/ @pschmitt @pvizeli @pree /tests/components/nuki/ @pschmitt @pvizeli @pree /homeassistant/components/numato/ @clssn diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index c731e3472d6db0..0c053a8c67ff3e 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -1,7 +1,5 @@ """Support for NuHeat thermostats.""" -from datetime import datetime import logging -import time from typing import Any from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD @@ -27,16 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - DOMAIN, - MANUFACTURER, - NUHEAT_API_STATE_SHIFT_DELAY, - NUHEAT_DATETIME_FORMAT, - NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME, - NUHEAT_KEY_SCHEDULE_MODE, - NUHEAT_KEY_SET_POINT_TEMP, - TEMP_HOLD_TIME_SEC, -) +from .const import DOMAIN, MANUFACTURER, NUHEAT_API_STATE_SHIFT_DELAY _LOGGER = logging.getLogger(__name__) @@ -225,22 +214,9 @@ def _set_temperature_and_mode(self, temperature, hvac_mode=None, preset_mode=Non target_schedule_mode, ) - target_temperature = max( - min(self._thermostat.max_temperature, target_temperature), - self._thermostat.min_temperature, + self._thermostat.set_target_temperature( + target_temperature, target_schedule_mode ) - - request = { - NUHEAT_KEY_SET_POINT_TEMP: target_temperature, - NUHEAT_KEY_SCHEDULE_MODE: target_schedule_mode, - } - - if target_schedule_mode == SCHEDULE_TEMPORARY_HOLD: - request[NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME] = datetime.fromtimestamp( - time.time() + TEMP_HOLD_TIME_SEC - ).strftime(NUHEAT_DATETIME_FORMAT) - - self._thermostat.set_data(request) self._schedule_mode = target_schedule_mode self._target_temperature = target_temperature self._schedule_update() diff --git a/homeassistant/components/nuheat/const.py b/homeassistant/components/nuheat/const.py index 619d4a11e2a030..ea43c33d9b0400 100644 --- a/homeassistant/components/nuheat/const.py +++ b/homeassistant/components/nuheat/const.py @@ -10,10 +10,3 @@ MANUFACTURER = "NuHeat" NUHEAT_API_STATE_SHIFT_DELAY = 2 - -TEMP_HOLD_TIME_SEC = 43200 - -NUHEAT_KEY_SET_POINT_TEMP = "SetPointTemp" -NUHEAT_KEY_SCHEDULE_MODE = "ScheduleMode" -NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME = "HoldSetPointDateTime" -NUHEAT_DATETIME_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index aea63a692a5cd7..90d18b87af2d0e 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -2,8 +2,8 @@ "domain": "nuheat", "name": "NuHeat", "documentation": "https://www.home-assistant.io/integrations/nuheat", - "requirements": ["nuheat==0.3.0"], - "codeowners": [], + "requirements": ["nuheat==1.0.0"], + "codeowners": ["@tstabrawa"], "config_flow": true, "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index bdebb6f8652623..ef90ca110e95d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ nsapi==3.0.5 nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat -nuheat==0.3.0 +nuheat==1.0.0 # homeassistant.components.numato numato-gpio==0.10.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c05a57e8508b44..010ff5a6b7d555 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -855,7 +855,7 @@ notify-events==1.0.4 nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat -nuheat==0.3.0 +nuheat==1.0.0 # homeassistant.components.numato numato-gpio==0.10.0 diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 133c3a15ccb355..4a6e9551b0bcd4 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -159,7 +159,7 @@ async def test_climate_thermostat_schedule_temporary_hold(hass): # opportunistic set state = hass.states.get("climate.temp_bathroom") assert state.attributes["preset_mode"] == "Temporary Hold" - assert state.attributes["temperature"] == 50.0 + assert state.attributes["temperature"] == 90.0 # and the api poll returns it to the mock async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=3)) From ff4456cb295f52d432db166d474d567299d98b39 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Nov 2022 08:24:49 +0100 Subject: [PATCH 263/394] Improve MQTT type hints part 4 (#80971) * Improve typing humidifier * Improve typing lock * Improve typing number * Set humidifier type hints at class level * Set lock type hints at class level * Set number type hints at class level * Some small updates * Follow up comment * Remove assert --- homeassistant/components/mqtt/humidifier.py | 100 ++++++++++++-------- homeassistant/components/mqtt/lock.py | 58 ++++++------ homeassistant/components/mqtt/number.py | 66 ++++++++----- 3 files changed, 128 insertions(+), 96 deletions(-) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index e3e94c07daeafc..69b6d3e3e89628 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -1,6 +1,7 @@ """Support for MQTT humidifiers.""" from __future__ import annotations +from collections.abc import Callable import functools import logging from typing import Any @@ -28,6 +29,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -50,7 +52,13 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" @@ -87,18 +95,18 @@ _LOGGER = logging.getLogger(__name__) -def valid_mode_configuration(config): +def valid_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the mode reset payload is not one of the available modes.""" - if config.get(CONF_PAYLOAD_RESET_MODE) in config.get(CONF_AVAILABLE_MODES_LIST): + if config[CONF_PAYLOAD_RESET_MODE] in config[CONF_AVAILABLE_MODES_LIST]: raise ValueError("modes must not contain payload_reset_mode") return config -def valid_humidity_range_configuration(config): +def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: """Validate that the target_humidity range configuration is valid, throws if it isn't.""" - if config.get(CONF_TARGET_HUMIDITY_MIN) >= config.get(CONF_TARGET_HUMIDITY_MAX): + if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]: raise ValueError("target_humidity_max must be > target_humidity_min") - if config.get(CONF_TARGET_HUMIDITY_MAX) > 100: + if config[CONF_TARGET_HUMIDITY_MAX] > 100: raise ValueError("max_humidity must be <= 100") return config @@ -196,8 +204,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT humidifier.""" async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)]) @@ -209,30 +217,36 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): _entity_id_format = humidifier.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] + _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _optimistic: bool + _optimistic_target_humidity: bool + _optimistic_mode: bool + _payload: dict[str, str] + _topic: dict[str, Any] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT humidifier.""" self._attr_mode = None - self._topic = None - self._payload = None - self._value_templates = None - self._command_templates = None - self._optimistic = None - self._optimistic_target_humidity = None - self._optimistic_mode = None - MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._attr_min_humidity = config.get(CONF_TARGET_HUMIDITY_MIN) - self._attr_max_humidity = config.get(CONF_TARGET_HUMIDITY_MAX) + self._attr_min_humidity = config[CONF_TARGET_HUMIDITY_MIN] + self._attr_max_humidity = config[CONF_TARGET_HUMIDITY_MAX] self._topic = { key: config.get(key) @@ -245,16 +259,6 @@ def _setup_from_config(self, config): CONF_MODE_COMMAND_TOPIC, ) } - self._value_templates = { - CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), - ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE), - ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE), - } - self._command_templates = { - CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), - ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE), - ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE), - } self._payload = { "STATE_ON": config[CONF_PAYLOAD_ON], "STATE_OFF": config[CONF_PAYLOAD_OFF], @@ -270,31 +274,43 @@ def _setup_from_config(self, config): else: self._attr_supported_features = 0 - optimistic = config[CONF_OPTIMISTIC] + optimistic: bool = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None self._optimistic_target_humidity = ( optimistic or self._topic[CONF_TARGET_HUMIDITY_STATE_TOPIC] is None ) self._optimistic_mode = optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None - for key, tpl in self._command_templates.items(): + self._command_templates = {} + command_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_COMMAND_TEMPLATE), + ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE), + ATTR_MODE: config.get(CONF_MODE_COMMAND_TEMPLATE), + } + for key, tpl in command_templates.items(): self._command_templates[key] = MqttCommandTemplate( tpl, entity=self ).async_render - for key, tpl in self._value_templates.items(): + self._value_templates = {} + value_templates: dict[str, Template | None] = { + CONF_STATE: config.get(CONF_STATE_VALUE_TEMPLATE), + ATTR_HUMIDITY: config.get(CONF_TARGET_HUMIDITY_STATE_TEMPLATE), + ATTR_MODE: config.get(CONF_MODE_STATE_TEMPLATE), + } + for key, tpl in value_templates.items(): self._value_templates[key] = MqttValueTemplate( tpl, entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, Any] = {} @callback @log_messages(self.hass, self.entity_id) - def state_received(msg): + def state_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message.""" payload = self._value_templates[CONF_STATE](msg.payload) if not payload: @@ -318,7 +334,7 @@ def state_received(msg): @callback @log_messages(self.hass, self.entity_id) - def target_humidity_received(msg): + def target_humidity_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for the target humidity.""" rendered_target_humidity_payload = self._value_templates[ATTR_HUMIDITY]( msg.payload @@ -365,9 +381,9 @@ def target_humidity_received(msg): @callback @log_messages(self.hass, self.entity_id) - def mode_received(msg): + def mode_received(msg: ReceiveMessage) -> None: """Handle new received MQTT message for mode.""" - mode = self._value_templates[ATTR_MODE](msg.payload) + mode = str(self._value_templates[ATTR_MODE](msg.payload)) if mode == self._payload["MODE_RESET"]: self._attr_mode = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -375,7 +391,7 @@ def mode_received(msg): if not mode: _LOGGER.debug("Ignoring empty mode from '%s'", msg.topic) return - if mode not in self.available_modes: + if not self.available_modes or mode not in self.available_modes: _LOGGER.warning( "'%s' received on topic %s. '%s' is not a valid mode", msg.payload, @@ -400,7 +416,7 @@ def mode_received(msg): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index c9bdd696896017..e141fcbd693a68 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,6 +1,7 @@ """Support for MQTT locks.""" from __future__ import annotations +from collections.abc import Callable import functools from typing import Any @@ -32,7 +33,7 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType from .util import get_mqtt_data CONF_PAYLOAD_LOCK = "payload_lock" @@ -112,8 +113,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT Lock platform.""" async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) @@ -125,39 +126,50 @@ class MqttLock(MqttEntity, LockEntity): _entity_id_format = lock.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): - """Initialize the lock.""" - self._state = False - self._optimistic = False + _optimistic: bool + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the lock.""" + self._attr_is_locked = False MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._optimistic = config[CONF_OPTIMISTIC] self._value_template = MqttValueTemplate( - self._config.get(CONF_VALUE_TEMPLATE), + config.get(CONF_VALUE_TEMPLATE), entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + self._attr_supported_features = ( + LockEntityFeature.OPEN if CONF_PAYLOAD_OPEN in config else 0 + ) + + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" payload = self._value_template(msg.payload) if payload == self._config[CONF_STATE_LOCKED]: - self._state = True + self._attr_is_locked = True elif payload == self._config[CONF_STATE_UNLOCKED]: - self._state = False + self._attr_is_locked = False get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -178,25 +190,15 @@ def message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - @property - def is_locked(self) -> bool: - """Return true if lock is locked.""" - return self._state - @property def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic - @property - def supported_features(self) -> int: - """Flag supported features.""" - return LockEntityFeature.OPEN if CONF_PAYLOAD_OPEN in self._config else 0 - async def async_lock(self, **kwargs: Any) -> None: """Lock the device. @@ -211,7 +213,7 @@ async def async_lock(self, **kwargs: Any) -> None: ) if self._optimistic: # Optimistically assume that the lock has changed state. - self._state = True + self._attr_is_locked = True self.async_write_ha_state() async def async_unlock(self, **kwargs: Any) -> None: @@ -228,7 +230,7 @@ async def async_unlock(self, **kwargs: Any) -> None: ) if self._optimistic: # Optimistically assume that the lock has changed state. - self._state = False + self._attr_is_locked = False self.async_write_ha_state() async def async_open(self, **kwargs: Any) -> None: @@ -245,5 +247,5 @@ async def async_open(self, **kwargs: Any) -> None: ) if self._optimistic: # Optimistically assume that the lock unlocks when opened. - self._state = False + self._attr_is_locked = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 95dbe970430f8a..51b480c036e5cc 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,6 +1,7 @@ """Configure number in a device through MQTT topic.""" from __future__ import annotations +from collections.abc import Callable import functools import logging @@ -47,7 +48,13 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -70,9 +77,9 @@ ) -def validate_config(config): +def validate_config(config: ConfigType) -> ConfigType: """Validate that the configuration is valid, throws if it isn't.""" - if config.get(CONF_MIN) >= config.get(CONF_MAX): + if config[CONF_MIN] >= config[CONF_MAX]: raise vol.Invalid(f"'{CONF_MAX}' must be > '{CONF_MIN}'") return config @@ -147,8 +154,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT number.""" async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)]) @@ -160,33 +167,39 @@ class MqttNumber(MqttEntity, RestoreNumber): _entity_id_format = number.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _optimistic: bool + _command_template: Callable[[PublishPayloadType], PublishPayloadType] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT Number.""" - self._config = config - self._optimistic = False - self._sub_state = None - RestoreNumber.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._config = config self._optimistic = config[CONF_OPTIMISTIC] - self._templates = { - CONF_COMMAND_TEMPLATE: MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), entity=self - ).async_render, - CONF_VALUE_TEMPLATE: MqttValueTemplate( - config.get(CONF_VALUE_TEMPLATE), - entity=self, - ).async_render_with_possible_json_value, - } + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), entity=self + ).async_render + self._value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), + entity=self, + ).async_render_with_possible_json_value + self._attr_device_class = config.get(CONF_DEVICE_CLASS) self._attr_mode = config[CONF_MODE] self._attr_native_max_value = config[CONF_MAX] @@ -194,14 +207,15 @@ def _setup_from_config(self, config): self._attr_native_step = config[CONF_STEP] self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) + num_value: int | float | None + payload = str(self._value_template(msg.payload)) try: if payload == self._config[CONF_PAYLOAD_RESET]: num_value = None @@ -245,7 +259,7 @@ def message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -260,7 +274,7 @@ async def async_set_native_value(self, value: float) -> None: if value.is_integer(): current_number = int(value) - payload = self._templates[CONF_COMMAND_TEMPLATE](current_number) + payload = self._command_template(current_number) if self._optimistic: self._attr_native_value = current_number From efd60de1ac41c4da68b3ebaa8bd6385a4bb26c0b Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 7 Nov 2022 19:01:07 +1100 Subject: [PATCH 264/394] Add integration_type to geonetnz_volcano (#81607) define integration type --- homeassistant/components/geonetnz_volcano/manifest.json | 3 ++- homeassistant/generated/integrations.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index a365237561afe0..7c765ecb9394e4 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -6,5 +6,6 @@ "requirements": ["aio_geojson_geonetnz_volcano==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["aio_geojson_geonetnz_volcano"] + "loggers": ["aio_geojson_geonetnz_volcano"], + "integration_type": "service" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 55a330685d507e..89de4fa92fca43 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -1841,7 +1841,7 @@ "name": "GeoNet NZ Quakes" }, "geonetnz_volcano": { - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "GeoNet NZ Volcano" From 9bc029000aa123fb645392c8aca4a6f1b6be6704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 7 Nov 2022 10:09:47 +0200 Subject: [PATCH 265/394] Upgrade prettier to v2.7.1 (#81682) No formatting changes. https://github.com/prettier/prettier/blob/2.7.1/CHANGELOG.md --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 751c97ebbb479e..1a2e4d15482a1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -65,7 +65,7 @@ repos: hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.6.1 + rev: v2.7.1 hooks: - id: prettier - repo: https://github.com/cdce8p/python-typing-update From 5e05739019774f0647f8e442dccfd210ae168cf0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Nov 2022 09:41:53 +0100 Subject: [PATCH 266/394] Add type hints to template helper (#81308) * Add type hints to template helper * Update homeassistant/helpers/template.py * Adjust use of ensure_compiled --- homeassistant/helpers/template.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index dfab80e5223c71..24852ab5cf3b54 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -559,8 +559,11 @@ def render_with_possible_json_value(self, value, error_value=_SENTINEL): @callback def async_render_with_possible_json_value( - self, value, error_value=_SENTINEL, variables=None - ): + self, + value: Any, + error_value: Any = _SENTINEL, + variables: dict[str, Any] | None = None, + ) -> Any: """Render template with value exposed. If valid JSON will expose value_json too. @@ -570,8 +573,7 @@ def async_render_with_possible_json_value( if self.is_static: return self.template - if self._compiled is None: - self._ensure_compiled() + compiled = self._compiled or self._ensure_compiled() variables = dict(variables or {}) variables["value"] = value @@ -580,9 +582,7 @@ def async_render_with_possible_json_value( variables["value_json"] = json_loads(value) try: - return _render_with_context( - self.template, self._compiled, **variables - ).strip() + return _render_with_context(self.template, compiled, **variables).strip() except jinja2.TemplateError as ex: if error_value is _SENTINEL: _LOGGER.error( From 1d633ac484e61564dc6fdf87030072cc69bb480c Mon Sep 17 00:00:00 2001 From: Benjamin Salchow <43908980+benjamin-salchow@users.noreply.github.com> Date: Mon, 7 Nov 2022 10:14:12 +0100 Subject: [PATCH 267/394] Accept input register in Modbus binary sensor (#81352) Adds input register as valid option for modbus binary_sensor Co-authored-by: jan iversen --- homeassistant/components/modbus/__init__.py | 7 ++++++- tests/components/modbus/test_binary_sensor.py | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 8aa2903506fd16..cda0bb64703b54 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -287,7 +287,12 @@ { vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_INPUT_TYPE, default=CALL_TYPE_COIL): vol.In( - [CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING] + [ + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + ] ), vol.Optional(CONF_SLAVE_COUNT, default=0): cv.positive_int, } diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 777d284e20f3c4..cdd019c73f34cb 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -6,6 +6,7 @@ CALL_TYPE_COIL, CALL_TYPE_DISCRETE, CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, CONF_INPUT_TYPE, CONF_LAZY_ERROR, CONF_SLAVE_COUNT, @@ -54,6 +55,16 @@ } ] }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_SLAVE: 10, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + } + ] + }, ], ) async def test_config_binary_sensor(hass, mock_modbus): @@ -91,6 +102,15 @@ async def test_config_binary_sensor(hass, mock_modbus): }, ], }, + { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + }, + ], + }, ], ) @pytest.mark.parametrize( From 11a55d6d4c22eae254c2781405b2f4c483111cb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 04:41:02 -0600 Subject: [PATCH 268/394] Fix flapping logbook tests (#81695) --- .../components/logbook/test_websocket_api.py | 91 ++++++++++++++----- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index fb0defca93c39e..42f604b3d4cbfd 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -24,6 +24,7 @@ CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, @@ -52,6 +53,15 @@ def set_utc(hass): hass.config.set_time_zone("UTC") +def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: + """Return listeners without final write listeners since we are not testing for these.""" + return { + key: value + for key, value in listeners.items() + if key != EVENT_HOMEASSISTANT_FINAL_WRITE + } + + async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" @@ -684,7 +694,9 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -892,7 +904,9 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1083,7 +1097,9 @@ async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1386,7 +1402,9 @@ async def test_subscribe_unsubscribe_logbook_stream( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1484,7 +1502,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1586,12 +1606,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( assert msg["success"] # Check our listener got unsubscribed - listeners = hass.bus.async_listeners() - # The async_fire_time_changed above triggers unsubscribe from - # homeassistant_final_write, don't worry about those - init_listeners.pop("homeassistant_final_write") - listeners.pop("homeassistant_final_write") - assert listeners == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1659,7 +1676,9 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1759,7 +1778,9 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1853,7 +1874,9 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) async def test_event_stream_bad_start_time(recorder_mock, hass, hass_ws_client): @@ -1968,7 +1991,9 @@ async def test_logbook_stream_match_multiple_entities( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) async def test_event_stream_bad_end_time(recorder_mock, hass, hass_ws_client): @@ -2091,7 +2116,9 @@ async def test_live_stream_with_one_second_commit_interval( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2146,7 +2173,9 @@ async def test_subscribe_disconnected(recorder_mock, hass, hass_ws_client): await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2189,7 +2218,9 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie assert msg["type"] == TYPE_RESULT assert msg["success"] - assert hass.bus.async_listeners() != init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) != listeners_without_writes(init_listeners) for _ in range(5): hass.states.async_set("binary_sensor.is_light", STATE_ON) hass.states.async_set("binary_sensor.is_light", STATE_OFF) @@ -2197,9 +2228,13 @@ async def test_stream_consumer_stop_processing(recorder_mock, hass, hass_ws_clie # Check our listener got unsubscribed because # the queue got full and the overload safety tripped - assert hass.bus.async_listeners() == after_ws_created_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(after_ws_created_listeners) await websocket_client.close() - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2332,7 +2367,9 @@ def _cycle_entities(): await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2494,7 +2531,9 @@ def _cycle_entities(): await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2608,7 +2647,9 @@ async def test_logbook_stream_ignores_forced_updates( assert msg["success"] # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -2703,4 +2744,6 @@ def _create_events(): await hass.async_block_till_done() # Check our listener got unsubscribed - assert hass.bus.async_listeners() == init_listeners + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) From 3184c8a5266d66cf65df69f58d723f692f4334e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 05:06:38 -0600 Subject: [PATCH 269/394] Fix use of deprecated device.rssi in bluetooth scanner (#81690) --- homeassistant/components/bluetooth/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 6b23cae02183e7..43c4706474aa33 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -209,7 +209,7 @@ def _async_detection_callback( service_info = BluetoothServiceInfoBleak( name=advertisement_data.local_name or device.name or device.address, address=device.address, - rssi=device.rssi, + rssi=advertisement_data.rssi, manufacturer_data=advertisement_data.manufacturer_data, service_data=advertisement_data.service_data, service_uuids=advertisement_data.service_uuids, From 2bea77549d02c61211decd62b7a5a0c8ee2e293a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 05:09:36 -0600 Subject: [PATCH 270/394] Fix creating multiple ElkM1 systems with TLS 1.2 (#81627) fixes https://github.com/home-assistant/core/issues/81516 --- homeassistant/components/elkm1/__init__.py | 9 +- homeassistant/components/elkm1/config_flow.py | 13 +- tests/components/elkm1/test_config_flow.py | 146 ++++++++++++++++++ 3 files changed, 159 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 7833bfd66b3b55..7b14c7e85ed27b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -7,11 +7,11 @@ import re from types import MappingProxyType from typing import Any, cast -from urllib.parse import urlparse import async_timeout from elkm1_lib.elements import Element from elkm1_lib.elk import Elk +from elkm1_lib.util import parse_url import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -96,6 +96,11 @@ ) +def hostname_from_url(url: str) -> str: + """Return the hostname from a url.""" + return parse_url(url)[1] + + def _host_validator(config: dict[str, str]) -> dict[str, str]: """Validate that a host is properly configured.""" if config[CONF_HOST].startswith("elks://"): @@ -231,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elk-M1 Control from a config entry.""" conf: MappingProxyType[str, Any] = entry.data - host = urlparse(entry.data[CONF_HOST]).hostname + host = hostname_from_url(entry.data[CONF_HOST]) _LOGGER.debug("Setting up elkm1 %s", conf["host"]) diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 8675ff45ee7087..ac7fc9033304c2 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -4,7 +4,6 @@ import asyncio import logging from typing import Any -from urllib.parse import urlparse from elkm1_lib.discovery import ElkSystem from elkm1_lib.elk import Elk @@ -26,7 +25,7 @@ from homeassistant.util import slugify from homeassistant.util.network import is_ip_address -from . import async_wait_for_elk_to_sync +from . import async_wait_for_elk_to_sync, hostname_from_url from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT from .discovery import ( _short_mac, @@ -170,7 +169,7 @@ async def _async_handle_discovery(self) -> FlowResult: for entry in self._async_current_entries(include_ignore=False): if ( entry.unique_id == mac - or urlparse(entry.data[CONF_HOST]).hostname == host + or hostname_from_url(entry.data[CONF_HOST]) == host ): if async_update_entry_from_discovery(self.hass, entry, device): self.hass.async_create_task( @@ -214,7 +213,7 @@ async def async_step_user( current_unique_ids = self._async_current_ids() current_hosts = { - urlparse(entry.data[CONF_HOST]).hostname + hostname_from_url(entry.data[CONF_HOST]) for entry in self._async_current_entries(include_ignore=False) } discovered_devices = await async_discover_devices( @@ -344,7 +343,7 @@ async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: if self._url_already_configured(url): return self.async_abort(reason="address_already_configured") - host = urlparse(url).hostname + host = hostname_from_url(url) _LOGGER.debug( "Importing is trying to fill unique id from discovery for %s", host ) @@ -367,10 +366,10 @@ async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: def _url_already_configured(self, url: str) -> bool: """See if we already have a elkm1 matching user input configured.""" existing_hosts = { - urlparse(entry.data[CONF_HOST]).hostname + hostname_from_url(entry.data[CONF_HOST]) for entry in self._async_current_entries() } - return urlparse(url).hostname in existing_hosts + return hostname_from_url(url) in existing_hosts class InvalidAuth(exceptions.HomeAssistantError): diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 7ce0e2163ac30d..a58528b700af5e 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1454,3 +1454,149 @@ async def test_multiple_instances_with_discovery(hass): "password": "", } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_multiple_instances_with_tls_v12(hass): + """Test we can setup a secure elk with tls v1_2.""" + + elk_discovery_1 = ElkSystem("aa:bb:cc:dd:ee:ff", "127.0.0.1", 2601) + elk_discovery_2 = ElkSystem("aa:bb:cc:dd:ee:fe", "127.0.0.2", 2601) + + with _patch_discovery(device=elk_discovery_1): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_1.mac_address}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert not result["errors"] + assert result2["step_id"] == "discovered_connection" + with _patch_discovery(device=elk_discovery_1), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "TLS 1.2", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeeff" + assert result3["data"] == { + "auto_configure": True, + "host": "elksv1_2://127.0.0.1", + "password": "test-password", + "prefix": "", + "username": "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # Now try to add another instance with the different discovery info + with _patch_discovery(device=elk_discovery_2): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert not result["errors"] + assert result["step_id"] == "user" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_elk(elk=mocked_elk): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"device": elk_discovery_2.mac_address}, + ) + await hass.async_block_till_done() + + with _patch_discovery(device=elk_discovery_2), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "protocol": "TLS 1.2", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "ElkM1 ddeefe" + assert result3["data"] == { + "auto_configure": True, + "host": "elksv1_2://127.0.0.2", + "password": "test-password", + "prefix": "ddeefe", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Finally, try to add another instance manually with no discovery info + + with _patch_discovery(no_device=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "manual_connection" + + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) + + with _patch_discovery(no_device=True), _patch_elk(elk=mocked_elk), patch( + "homeassistant.components.elkm1.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "TLS 1.2", + "address": "1.2.3.4", + "prefix": "guest_house", + "password": "test-password", + "username": "test-username", + }, + ) + await hass.async_block_till_done() + + import pprint + + pprint.pprint(result2) + assert result2["type"] == "create_entry" + assert result2["title"] == "guest_house" + assert result2["data"] == { + "auto_configure": True, + "host": "elksv1_2://1.2.3.4", + "prefix": "guest_house", + "password": "test-password", + "username": "test-username", + } + assert len(mock_setup_entry.mock_calls) == 1 From 190840cd335bec9b12a6bd338d0983cf6af1dd07 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 7 Nov 2022 06:12:41 -0500 Subject: [PATCH 271/394] Bump pyunifiprotect to 4.4.0 (#81696) --- homeassistant/components/unifiprotect/__init__.py | 4 ++-- homeassistant/components/unifiprotect/manifest.json | 3 ++- homeassistant/components/unifiprotect/strings.json | 4 ++-- .../components/unifiprotect/translations/en.json | 8 ++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 649d62c6feee3d..174c5476cff67e 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -102,8 +102,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) - protect_version = data_service.api.bootstrap.nvr.version - if protect_version.is_prerelease: + if await data_service.api.bootstrap.get_is_prerelease(): + protect_version = data_service.api.bootstrap.nvr.version ir.async_create_issue( hass, DOMAIN, diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ae37360c5eeabc..62baf74b6a785f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -1,9 +1,10 @@ { "domain": "unifiprotect", "name": "UniFi Protect", + "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.3.4", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.4.0", "unifi-discovery==1.1.7"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index 2967ba8c82eb9e..5c9416440de7d2 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -57,8 +57,8 @@ }, "issues": { "ea_warning": { - "title": "{version} is an Early Access version of UniFi Protect", - "description": "You are using {version} of UniFi Protect. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected." + "title": "UniFi Protect v{version} is an Early Access version", + "description": "You are using v{version} of UniFi Protect which is an Early Access version. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected." } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 3b6e74db341d28..033626298796b4 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -43,20 +43,16 @@ }, "issues": { "ea_warning": { - "description": "You are using {version} of UniFi Protect. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected.", - "title": "{version} is an Early Access version of UniFi Protect" + "description": "You are using v{version} of UniFi Protect which is an Early Access version. Early Access versions are not supported by Home Assistant and may cause your UniFi Protect integration to break or not work as expected.", + "title": "UniFi Protect v{version} is an Early Access version" } }, "options": { - "error": { - "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" - }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", - "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/requirements_all.txt b/requirements_all.txt index ef90ca110e95d0..08c7fa7a7acc19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2084,7 +2084,7 @@ pytrafikverket==0.2.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.3.4 +pyunifiprotect==4.4.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 010ff5a6b7d555..34c694a23f3525 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1447,7 +1447,7 @@ pytrafikverket==0.2.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.3.4 +pyunifiprotect==4.4.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 9860b0691353ec954d44bab829fcae5efd0868f7 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 7 Nov 2022 06:23:49 -0500 Subject: [PATCH 272/394] Bump aiopyarr to 22.11.0 (#81694) --- homeassistant/components/lidarr/manifest.json | 2 +- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index 4c07e0e17629af..eab24ef7e423f4 100644 --- a/homeassistant/components/lidarr/manifest.json +++ b/homeassistant/components/lidarr/manifest.json @@ -2,7 +2,7 @@ "domain": "lidarr", "name": "Lidarr", "documentation": "https://www.home-assistant.io/integrations/lidarr", - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 9b140def96ad12..5117fd161d3d2f 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -2,7 +2,7 @@ "domain": "radarr", "name": "Radarr", "documentation": "https://www.home-assistant.io/integrations/radarr", - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index daf9e20586b267..511842a3e99a85 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.10.0"], + "requirements": ["aiopyarr==22.11.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 08c7fa7a7acc19..550dc61e18f929 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -240,7 +240,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.10.0 +aiopyarr==22.11.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34c694a23f3525..e626dfe50b11f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -215,7 +215,7 @@ aiopvpc==3.0.0 # homeassistant.components.lidarr # homeassistant.components.radarr # homeassistant.components.sonarr -aiopyarr==22.10.0 +aiopyarr==22.11.0 # homeassistant.components.qnap_qsw aioqsw==0.2.2 From d1fd141e8c1d1e34f35a187a70fde13caee57589 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 7 Nov 2022 06:28:59 -0500 Subject: [PATCH 273/394] Bump ZHA quirks and associated changes (#81587) --- .../zha/core/channels/manufacturerspecific.py | 74 ++++++++----------- homeassistant/components/zha/device_action.py | 31 +++++--- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zha/switch.py | 12 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zha/test_device_action.py | 2 +- 7 files changed, 67 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 814e7700d01e04..c4baccf4ae6cc7 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -4,7 +4,7 @@ import logging from typing import TYPE_CHECKING, Any -from zigpy import types +from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType from zigpy.exceptions import ZigbeeException import zigpy.zcl @@ -183,59 +183,47 @@ def cluster_command(self, tsn, command_id, args): class InovelliConfigEntityChannel(ZigbeeChannel): """Inovelli Configuration Entity channel.""" - class LEDEffectType(types.enum8): - """Effect type for Inovelli Blue Series switch.""" - - Off = 0x00 - Solid = 0x01 - Fast_Blink = 0x02 - Slow_Blink = 0x03 - Pulse = 0x04 - Chase = 0x05 - Open_Close = 0x06 - Small_To_Big = 0x07 - Clear = 0xFF - REPORT_CONFIG = () ZCL_INIT_ATTRS = { - "dimming_speed_up_remote": False, - "dimming_speed_up_local": False, - "ramp_rate_off_to_on_local": False, - "ramp_rate_off_to_on_remote": False, - "dimming_speed_down_remote": False, - "dimming_speed_down_local": False, - "ramp_rate_on_to_off_local": False, - "ramp_rate_on_to_off_remote": False, - "minimum_level": False, - "maximum_level": False, - "invert_switch": False, - "auto_off_timer": False, - "default_level_local": False, - "default_level_remote": False, - "state_after_power_restored": False, - "load_level_indicator_timeout": False, - "active_power_reports": False, - "periodic_power_and_energy_reports": False, - "active_energy_reports": False, + "dimming_speed_up_remote": True, + "dimming_speed_up_local": True, + "ramp_rate_off_to_on_local": True, + "ramp_rate_off_to_on_remote": True, + "dimming_speed_down_remote": True, + "dimming_speed_down_local": True, + "ramp_rate_on_to_off_local": True, + "ramp_rate_on_to_off_remote": True, + "minimum_level": True, + "maximum_level": True, + "invert_switch": True, + "auto_off_timer": True, + "default_level_local": True, + "default_level_remote": True, + "state_after_power_restored": True, + "load_level_indicator_timeout": True, + "active_power_reports": True, + "periodic_power_and_energy_reports": True, + "active_energy_reports": True, "power_type": False, "switch_type": False, "button_delay": False, "smart_bulb_mode": False, - "double_tap_up_for_full_brightness": False, - "led_color_when_on": False, - "led_color_when_off": False, - "led_intensity_when_on": False, - "led_intensity_when_off": False, + "double_tap_up_for_full_brightness": True, + "led_color_when_on": True, + "led_color_when_off": True, + "led_intensity_when_on": True, + "led_intensity_when_off": True, "local_protection": False, "output_mode": False, - "on_off_led_mode": False, - "firmware_progress_led": False, - "relay_click_in_on_off_mode": False, + "on_off_led_mode": True, + "firmware_progress_led": True, + "relay_click_in_on_off_mode": True, + "disable_clear_notifications_double_tap": True, } async def issue_all_led_effect( self, - effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, + effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink, color: int = 200, level: int = 100, duration: int = 3, @@ -251,7 +239,7 @@ async def issue_all_led_effect( async def issue_individual_led_effect( self, led_number: int = 1, - effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink, + effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink, color: int = 200, level: int = 100, duration: int = 3, diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 3e2a3591c804cf..01a08bc2f32bcc 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -13,7 +13,7 @@ from . import DOMAIN from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN -from .core.channels.manufacturerspecific import InovelliConfigEntityChannel +from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI from .core.helpers import async_get_zha_device @@ -40,9 +40,7 @@ { vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT, vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required( - "effect_type" - ): InovelliConfigEntityChannel.LEDEffectType.__getitem__, + vol.Required("effect_type"): AllLEDEffectType.__getitem__, vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -52,10 +50,16 @@ INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend( { vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT, - vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)), + vol.Required("effect_type"): SingleLEDEffectType.__getitem__, + vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), } ) +ACTION_SCHEMA_MAP = { + INOVELLI_ALL_LED_EFFECT: INOVELLI_ALL_LED_EFFECT_SCHEMA, + INOVELLI_INDIVIDUAL_LED_EFFECT: INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA, +} + ACTION_SCHEMA = vol.Any( INOVELLI_ALL_LED_EFFECT_SCHEMA, INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA, @@ -83,9 +87,7 @@ DEVICE_ACTION_SCHEMAS = { INOVELLI_ALL_LED_EFFECT: vol.Schema( { - vol.Required("effect_type"): vol.In( - InovelliConfigEntityChannel.LEDEffectType.__members__.keys() - ), + vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()), vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -94,9 +96,7 @@ INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema( { vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)), - vol.Required("effect_type"): vol.In( - InovelliConfigEntityChannel.LEDEffectType.__members__.keys() - ), + vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()), vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)), vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)), vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)), @@ -127,6 +127,15 @@ async def async_call_action_from_config( ) +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA) + config = schema(config) + return config + + async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e40a54c11bce99..c8aebe3b0c0a66 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.84", + "zha-quirks==0.0.85", "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 0bd55cdbe68407..0c2e5e7ebe2ccb 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode( _zcl_attribute: str = "relay_click_in_on_off_mode" _attr_name: str = "Disable relay click in on off mode" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_INOVELLI, +) +class InovelliDisableDoubleTapClearNotificationsMode( + ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap" +): + """Inovelli disable clear notifications double tap control.""" + + _zcl_attribute: str = "disable_clear_notifications_double_tap" + _attr_name: str = "Disable config 2x tap to clear notifications" diff --git a/requirements_all.txt b/requirements_all.txt index 550dc61e18f929..0b1541dd0771b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2616,7 +2616,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.84 +zha-quirks==0.0.85 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e626dfe50b11f4..7061345c0f3f98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1817,7 +1817,7 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.84 +zha-quirks==0.0.85 # homeassistant.components.zha zigpy-deconz==0.19.0 diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 584abbaecdbc88..19125558b52e8b 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -290,7 +290,7 @@ async def test_action(hass, device_ias, device_inovelli): "domain": DOMAIN, "device_id": inovelli_reg_device.id, "type": "issue_individual_led_effect", - "effect_type": "Open_Close", + "effect_type": "Falling", "led_number": 1, "duration": 5, "level": 10, From 9b2a8901b17539d6b46b1c929d563686fb6d5794 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Nov 2022 12:31:11 +0100 Subject: [PATCH 274/394] Adjust payload sentinel in mqtt (#81553) * Adjust payload sentinel in mqtt * Add type hints * Update sensor.py * Adjust vacuum * Add type hints * Adjust schema basic * Remove invalid hint --- .../components/mqtt/light/schema_basic.py | 53 +++++++++++-------- homeassistant/components/mqtt/models.py | 13 +++-- homeassistant/components/mqtt/sensor.py | 33 +++++++----- .../components/mqtt/vacuum/schema_legacy.py | 40 +++++++++----- 4 files changed, 87 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index d435d4e91ad8a7..bf2ae33ca1d457 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -50,7 +50,12 @@ ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity -from ..models import MqttCommandTemplate, MqttValueTemplate +from ..models import ( + MqttCommandTemplate, + MqttValueTemplate, + PayloadSentinel, + ReceiveMessage, +) from ..util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA @@ -450,12 +455,12 @@ def state_received(msg): @callback @log_messages(self.hass, self.entity_id) - def brightness_received(msg): + def brightness_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for the brightness.""" payload = self._value_templates[CONF_BRIGHTNESS_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty brightness message from '%s'", msg.topic) return @@ -468,8 +473,10 @@ def brightness_received(msg): def _rgbx_received(msg, template, color_mode, convert_color): """Handle new MQTT messages for RGBW and RGBWW.""" - payload = self._value_templates[template](msg.payload, None) - if not payload: + payload = self._value_templates[template]( + msg.payload, PayloadSentinel.DEFAULT + ) + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug( "Ignoring empty %s message from '%s'", color_mode, msg.topic ) @@ -533,12 +540,12 @@ def rgbww_received(msg): @callback @log_messages(self.hass, self.entity_id) - def color_mode_received(msg): + def color_mode_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for color mode.""" payload = self._value_templates[CONF_COLOR_MODE_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty color mode message from '%s'", msg.topic) return @@ -549,12 +556,12 @@ def color_mode_received(msg): @callback @log_messages(self.hass, self.entity_id) - def color_temp_received(msg): + def color_temp_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for color temperature.""" payload = self._value_templates[CONF_COLOR_TEMP_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty color temp message from '%s'", msg.topic) return @@ -567,12 +574,12 @@ def color_temp_received(msg): @callback @log_messages(self.hass, self.entity_id) - def effect_received(msg): + def effect_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for effect.""" payload = self._value_templates[CONF_EFFECT_VALUE_TEMPLATE]( - msg.payload, None + msg.payload, PayloadSentinel.DEFAULT ) - if not payload: + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic) return @@ -583,10 +590,12 @@ def effect_received(msg): @callback @log_messages(self.hass, self.entity_id) - def hs_received(msg): + def hs_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for hs color.""" - payload = self._value_templates[CONF_HS_VALUE_TEMPLATE](msg.payload, None) - if not payload: + payload = self._value_templates[CONF_HS_VALUE_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return try: @@ -602,10 +611,12 @@ def hs_received(msg): @callback @log_messages(self.hass, self.entity_id) - def xy_received(msg): + def xy_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages for xy color.""" - payload = self._value_templates[CONF_XY_VALUE_TEMPLATE](msg.payload, None) - if not payload: + payload = self._value_templates[CONF_XY_VALUE_TEMPLATE]( + msg.payload, PayloadSentinel.DEFAULT + ) + if payload is PayloadSentinel.DEFAULT or not payload: _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 363956cc732408..a00097a68398d1 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -12,6 +12,7 @@ import attr +from homeassistant.backports.enum import StrEnum from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import template @@ -26,7 +27,13 @@ from .discovery import MQTTDiscoveryPayload from .tag import MQTTTagScanner -_SENTINEL = object() + +class PayloadSentinel(StrEnum): + """Sentinel for `async_render_with_possible_json_value`.""" + + NONE = "none" + DEFAULT = "default" + _LOGGER = logging.getLogger(__name__) @@ -189,7 +196,7 @@ def __init__( def async_render_with_possible_json_value( self, payload: ReceivePayloadType, - default: ReceivePayloadType | object = _SENTINEL, + default: ReceivePayloadType | PayloadSentinel = PayloadSentinel.NONE, variables: TemplateVarsType = None, ) -> ReceivePayloadType: """Render with possible json value or pass-though a received MQTT value.""" @@ -213,7 +220,7 @@ def async_render_with_possible_json_value( ) values[ATTR_THIS] = self._template_state - if default == _SENTINEL: + if default is PayloadSentinel.NONE: _LOGGER.debug( "Rendering incoming payload '%s' with variables %s and %s", payload, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 69077d30eee3e1..4c6b5409962fb2 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -45,7 +45,7 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -244,7 +244,7 @@ def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} - def _update_state(msg): + def _update_state(msg: ReceiveMessage) -> None: # auto-expire enabled? expire_after = self._config.get(CONF_EXPIRE_AFTER) if expire_after is not None and expire_after > 0: @@ -262,20 +262,25 @@ def _update_state(msg): self.hass, self._value_is_expired, expiration_at ) - payload = self._template(msg.payload, default=self.native_value) - - if payload is not None and self.device_class in ( + payload = self._template(msg.payload, default=PayloadSentinel.DEFAULT) + if payload is PayloadSentinel.DEFAULT: + return + if self.device_class not in { SensorDeviceClass.DATE, SensorDeviceClass.TIMESTAMP, - ): - if (payload := dt_util.parse_datetime(payload)) is None: - _LOGGER.warning( - "Invalid state message '%s' from '%s'", msg.payload, msg.topic - ) - elif self.device_class == SensorDeviceClass.DATE: - payload = payload.date() - - self._attr_native_value = payload + }: + self._attr_native_value = str(payload) + return + if (payload_datetime := dt_util.parse_datetime(str(payload))) is None: + _LOGGER.warning( + "Invalid state message '%s' from '%s'", msg.payload, msg.topic + ) + self._attr_native_value = None + return + if self.device_class == SensorDeviceClass.DATE: + self._attr_native_value = payload_datetime.date() + return + self._attr_native_value = payload_datetime def _update_last_reset(msg): payload = self._last_reset_template(msg.payload) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 09c4448fda70ac..a15367c3cadc19 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -19,7 +19,7 @@ from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema -from ..models import MqttValueTemplate +from ..models import MqttValueTemplate, PayloadSentinel, ReceiveMessage from ..util import get_mqtt_data, valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -246,7 +246,7 @@ def _prepare_subscribe_topics(self): @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT message.""" if ( msg.topic == self._state_topics[CONF_BATTERY_LEVEL_TOPIC] @@ -254,8 +254,10 @@ def message_received(msg): ): battery_level = self._templates[ CONF_BATTERY_LEVEL_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if battery_level: + ].async_render_with_possible_json_value( + msg.payload, PayloadSentinel.DEFAULT + ) + if battery_level and battery_level is not PayloadSentinel.DEFAULT: self._battery_level = int(battery_level) if ( @@ -264,8 +266,10 @@ def message_received(msg): ): charging = self._templates[ CONF_CHARGING_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if charging: + ].async_render_with_possible_json_value( + msg.payload, PayloadSentinel.DEFAULT + ) + if charging and charging is not PayloadSentinel.DEFAULT: self._charging = cv.boolean(charging) if ( @@ -274,8 +278,10 @@ def message_received(msg): ): cleaning = self._templates[ CONF_CLEANING_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if cleaning: + ].async_render_with_possible_json_value( + msg.payload, PayloadSentinel.DEFAULT + ) + if cleaning and cleaning is not PayloadSentinel.DEFAULT: self._cleaning = cv.boolean(cleaning) if ( @@ -284,8 +290,10 @@ def message_received(msg): ): docked = self._templates[ CONF_DOCKED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if docked: + ].async_render_with_possible_json_value( + msg.payload, PayloadSentinel.DEFAULT + ) + if docked and docked is not PayloadSentinel.DEFAULT: self._docked = cv.boolean(docked) if ( @@ -294,8 +302,10 @@ def message_received(msg): ): error = self._templates[ CONF_ERROR_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if error is not None: + ].async_render_with_possible_json_value( + msg.payload, PayloadSentinel.DEFAULT + ) + if error is not PayloadSentinel.DEFAULT: self._error = cv.string(error) if self._docked: @@ -316,8 +326,10 @@ def message_received(msg): ): fan_speed = self._templates[ CONF_FAN_SPEED_TEMPLATE - ].async_render_with_possible_json_value(msg.payload, None) - if fan_speed: + ].async_render_with_possible_json_value( + msg.payload, PayloadSentinel.DEFAULT + ) + if fan_speed and fan_speed is not PayloadSentinel.DEFAULT: self._fan_speed = fan_speed get_mqtt_data(self.hass).state_write_requests.write_state_request(self) From 934cec9778c220355bc3d407bf062a668aa81c92 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Nov 2022 13:08:05 +0100 Subject: [PATCH 275/394] Modernize rest switch tests (#81306) * Adjust rest switch tests to use schema validation * Simplify * Use async_setup_component in tests * Rewrite tests * Add patch * Remove patch * Adjust mock --- tests/components/rest/test_switch.py | 455 ++++++++++++++++----------- 1 file changed, 264 insertions(+), 191 deletions(-) diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 6275314bcf0414..ae7d507e8573d5 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -3,90 +3,114 @@ from http import HTTPStatus import aiohttp +import pytest from homeassistant.components.rest import DOMAIN -import homeassistant.components.rest.switch as rest -from homeassistant.components.switch import SwitchDeviceClass +from homeassistant.components.rest.switch import ( + CONF_BODY_OFF, + CONF_BODY_ON, + CONF_STATE_RESOURCE, +) +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SCAN_INTERVAL, + SwitchDeviceClass, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + ATTR_ICON, CONF_DEVICE_CLASS, CONF_HEADERS, + CONF_ICON, + CONF_METHOD, CONF_NAME, CONF_PARAMS, CONF_PLATFORM, CONF_RESOURCE, + CONF_UNIQUE_ID, CONTENT_TYPE_JSON, - Platform, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import CONF_PICTURE from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from tests.common import assert_setup_component +from tests.common import assert_setup_component, async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMocker NAME = "foo" DEVICE_CLASS = SwitchDeviceClass.SWITCH -METHOD = "post" RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE -PARAMS = None -async def test_setup_missing_config(hass: HomeAssistant) -> None: +async def test_setup_missing_config( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup with configuration missing required entries.""" - assert not await rest.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "Invalid config for [switch.rest]: required key not provided" in caplog.text -async def test_setup_missing_schema(hass: HomeAssistant) -> None: +async def test_setup_missing_schema( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: """Test setup with resource missing schema.""" - assert not await rest.async_setup_platform( - hass, - {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"}, - None, - ) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "localhost"}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "Invalid config for [switch.rest]: invalid url" in caplog.text async def test_setup_failed_connect( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection error occurs.""" - aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) - assert not await rest.async_setup_platform( - hass, - {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"}, - None, - ) + aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "No route to resource/endpoint" in caplog.text async def test_setup_timeout( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + caplog: pytest.LogCaptureFixture, ) -> None: """Test setup when connection timeout occurs.""" - aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) - assert not await rest.async_setup_platform( - hass, - {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: "http://localhost"}, - None, - ) + aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} + assert await async_setup_component(hass, SWITCH_DOMAIN, config) + await hass.async_block_till_done() + assert_setup_component(0, SWITCH_DOMAIN) + assert "No route to resource/endpoint" in caplog.text async def test_setup_minimum( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with minimum configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) - with assert_setup_component(1, Platform.SWITCH): - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_RESOURCE: "http://localhost", - } - }, - ) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + config = {SWITCH_DOMAIN: {CONF_PLATFORM: DOMAIN, CONF_RESOURCE: RESOURCE}} + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 @@ -96,18 +120,15 @@ async def test_setup_query_params( ) -> None: """Test setup with query params.""" aioclient_mock.get("http://localhost/?search=something", status=HTTPStatus.OK) - with assert_setup_component(1, Platform.SWITCH): - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_RESOURCE: "http://localhost", - CONF_PARAMS: {"search": "something"}, - } - }, - ) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_RESOURCE: RESOURCE, + CONF_PARAMS: {"search": "something"}, + } + } + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 @@ -115,244 +136,296 @@ async def test_setup_query_params( async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_NAME: "foo", - CONF_RESOURCE: "http://localhost", - CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, - rest.CONF_BODY_ON: "custom on text", - rest.CONF_BODY_OFF: "custom off text", - } - }, - ) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: RESOURCE, + CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, + CONF_BODY_ON: "custom on text", + CONF_BODY_OFF: "custom off text", + } + } + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 - assert_setup_component(1, Platform.SWITCH) + assert_setup_component(1, SWITCH_DOMAIN) async def test_setup_with_state_resource( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.NOT_FOUND) + aioclient_mock.get(RESOURCE, status=HTTPStatus.NOT_FOUND) aioclient_mock.get("http://localhost/state", status=HTTPStatus.OK) - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_NAME: "foo", - CONF_RESOURCE: "http://localhost", - rest.CONF_STATE_RESOURCE: "http://localhost/state", - CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, - rest.CONF_BODY_ON: "custom on text", - rest.CONF_BODY_OFF: "custom off text", - } - }, - ) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: RESOURCE, + CONF_STATE_RESOURCE: "http://localhost/state", + CONF_HEADERS: {"Content-type": CONTENT_TYPE_JSON}, + CONF_BODY_ON: "custom on text", + CONF_BODY_OFF: "custom off text", + } + } + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 - assert_setup_component(1, Platform.SWITCH) + assert_setup_component(1, SWITCH_DOMAIN) async def test_setup_with_templated_headers_params( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test setup with valid configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) - assert await async_setup_component( - hass, - Platform.SWITCH, - { - Platform.SWITCH: { - CONF_PLATFORM: DOMAIN, - CONF_NAME: "foo", - CONF_RESOURCE: "http://localhost", - CONF_HEADERS: { - "Accept": CONTENT_TYPE_JSON, - "User-Agent": "Mozilla/{{ 3 + 2 }}.0", - }, - CONF_PARAMS: { - "start": 0, - "end": "{{ 3 + 2 }}", - }, - } - }, - ) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + config = { + SWITCH_DOMAIN: { + CONF_PLATFORM: DOMAIN, + CONF_NAME: "foo", + CONF_RESOURCE: "http://localhost", + CONF_HEADERS: { + "Accept": CONTENT_TYPE_JSON, + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + }, + CONF_PARAMS: { + "start": 0, + "end": "{{ 3 + 2 }}", + }, + } + } + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 assert aioclient_mock.mock_calls[-1][3].get("Accept") == CONTENT_TYPE_JSON assert aioclient_mock.mock_calls[-1][3].get("User-Agent") == "Mozilla/5.0" assert aioclient_mock.mock_calls[-1][1].query["start"] == "0" assert aioclient_mock.mock_calls[-1][1].query["end"] == "5" - assert_setup_component(1, Platform.SWITCH) + assert_setup_component(1, SWITCH_DOMAIN) -"""Tests for REST switch platform.""" +# Tests for REST switch platform. -def _setup_test_switch(hass: HomeAssistant) -> None: - body_on = Template("on", hass) - body_off = Template("off", hass) - headers = {"Content-type": Template(CONTENT_TYPE_JSON, hass)} - switch = rest.RestSwitch( - hass, - { - CONF_NAME: Template(NAME, hass), - CONF_DEVICE_CLASS: DEVICE_CLASS, - CONF_RESOURCE: RESOURCE, - rest.CONF_STATE_RESOURCE: STATE_RESOURCE, - rest.CONF_METHOD: METHOD, - rest.CONF_HEADERS: headers, - rest.CONF_PARAMS: PARAMS, - rest.CONF_BODY_ON: body_on, - rest.CONF_BODY_OFF: body_off, - rest.CONF_IS_ON_TEMPLATE: None, - rest.CONF_TIMEOUT: 10, - rest.CONF_VERIFY_SSL: True, - }, - None, - ) - switch.hass = hass - return switch, body_on, body_off +async def _async_setup_test_switch( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) + + headers = {"Content-type": CONTENT_TYPE_JSON} + config = { + CONF_PLATFORM: DOMAIN, + CONF_NAME: NAME, + CONF_DEVICE_CLASS: DEVICE_CLASS, + CONF_RESOURCE: RESOURCE, + CONF_STATE_RESOURCE: STATE_RESOURCE, + CONF_HEADERS: headers, + } + assert await async_setup_component(hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: config}) + await hass.async_block_till_done() + assert_setup_component(1, SWITCH_DOMAIN) -def test_name(hass: HomeAssistant) -> None: - """Test the name.""" - switch, body_on, body_off = _setup_test_switch(hass) - assert switch.name == NAME + assert hass.states.get("switch.foo").state == STATE_UNKNOWN + aioclient_mock.clear_requests() -def test_device_class(hass: HomeAssistant) -> None: +async def test_name(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test the name.""" - switch, body_on, body_off = _setup_test_switch(hass) - assert switch.device_class == DEVICE_CLASS + await _async_setup_test_switch(hass, aioclient_mock) + + state = hass.states.get("switch.foo") + assert state.attributes[ATTR_FRIENDLY_NAME] == NAME -def test_is_on_before_update(hass: HomeAssistant) -> None: +async def test_device_class( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the device class.""" + await _async_setup_test_switch(hass, aioclient_mock) + + state = hass.states.get("switch.foo") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS + + +async def test_is_on_before_update( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test is_on in initial state.""" - switch, body_on, body_off = _setup_test_switch(hass) - assert switch.is_on is None + await _async_setup_test_switch(hass, aioclient_mock) + + state = hass.states.get("switch.foo") + assert state.state == STATE_UNKNOWN async def test_turn_on_success( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test turn_on.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert body_on.template == aioclient_mock.mock_calls[-1][2].decode() - assert switch.is_on + assert aioclient_mock.mock_calls[-2][2].decode() == "ON" + assert hass.states.get("switch.foo").state == STATE_ON async def test_turn_on_status_not_ok( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test turn_on when error status returned.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert body_on.template == aioclient_mock.mock_calls[-1][2].decode() - assert switch.is_on is None + assert aioclient_mock.mock_calls[-1][2].decode() == "ON" + assert hass.states.get("switch.foo").state == STATE_UNKNOWN async def test_turn_on_timeout( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test turn_on when timeout occurs.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert switch.is_on is None + assert hass.states.get("switch.foo").state == STATE_UNKNOWN async def test_turn_off_success( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test turn_off.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.OK) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_off() + aioclient_mock.get(RESOURCE, exc=aiohttp.ClientError) + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert aioclient_mock.mock_calls[-2][2].decode() == "OFF" - assert body_off.template == aioclient_mock.mock_calls[-1][2].decode() - assert not switch.is_on + assert hass.states.get("switch.foo").state == STATE_OFF async def test_turn_off_status_not_ok( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test turn_off when error status returned.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, status=HTTPStatus.INTERNAL_SERVER_ERROR) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_off() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert body_off.template == aioclient_mock.mock_calls[-1][2].decode() - assert switch.is_on is None + assert aioclient_mock.mock_calls[-1][2].decode() == "OFF" + + assert hass.states.get("switch.foo").state == STATE_UNKNOWN async def test_turn_off_timeout( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test turn_off when timeout occurs.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.post(RESOURCE, exc=asyncio.TimeoutError()) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_turn_on() + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.foo"}, + blocking=True, + ) + await hass.async_block_till_done() - assert switch.is_on is None + assert hass.states.get("switch.foo").state == STATE_UNKNOWN async def test_update_when_on( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test update when switch is on.""" - switch, body_on, body_off = _setup_test_switch(hass) - aioclient_mock.get(RESOURCE, text=body_on.template) - await switch.async_update() + await _async_setup_test_switch(hass, aioclient_mock) - assert switch.is_on + aioclient_mock.get(RESOURCE, text="ON") + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + assert hass.states.get("switch.foo").state == STATE_ON async def test_update_when_off( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test update when switch is off.""" - switch, body_on, body_off = _setup_test_switch(hass) - aioclient_mock.get(RESOURCE, text=body_off.template) - await switch.async_update() + await _async_setup_test_switch(hass, aioclient_mock) + + aioclient_mock.get(RESOURCE, text="OFF") + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() - assert not switch.is_on + assert hass.states.get("switch.foo").state == STATE_OFF async def test_update_when_unknown( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test update when unknown status returned.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.get(RESOURCE, text="unknown status") - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_update() + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() - assert switch.is_on is None + assert hass.states.get("switch.foo").state == STATE_UNKNOWN async def test_update_timeout( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test update when timeout occurs.""" + await _async_setup_test_switch(hass, aioclient_mock) + aioclient_mock.get(RESOURCE, exc=asyncio.TimeoutError()) - switch, body_on, body_off = _setup_test_switch(hass) - await switch.async_update() + async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() - assert switch.is_on is None + assert hass.states.get("switch.foo").state == STATE_UNKNOWN async def test_entity_config( @@ -360,22 +433,22 @@ async def test_entity_config( ) -> None: """Test entity configuration.""" - aioclient_mock.get("http://localhost", status=HTTPStatus.OK) + aioclient_mock.get(RESOURCE, status=HTTPStatus.OK) config = { - Platform.SWITCH: { + SWITCH_DOMAIN: { # REST configuration - "platform": "rest", - "method": "POST", - "resource": "http://localhost", + CONF_PLATFORM: "rest", + CONF_METHOD: "POST", + CONF_RESOURCE: "http://localhost", # Entity configuration - "icon": "{{'mdi:one_two_three'}}", - "picture": "{{'blabla.png'}}", - "name": "{{'REST' + ' ' + 'Switch'}}", - "unique_id": "very_unique", + CONF_ICON: "{{'mdi:one_two_three'}}", + CONF_PICTURE: "{{'blabla.png'}}", + CONF_NAME: "{{'REST' + ' ' + 'Switch'}}", + CONF_UNIQUE_ID: "very_unique", }, } - assert await async_setup_component(hass, Platform.SWITCH, config) + assert await async_setup_component(hass, SWITCH_DOMAIN, config) await hass.async_block_till_done() entity_registry = er.async_get(hass) @@ -384,7 +457,7 @@ async def test_entity_config( state = hass.states.get("switch.rest_switch") assert state.state == "unknown" assert state.attributes == { - "entity_picture": "blabla.png", - "friendly_name": "REST Switch", - "icon": "mdi:one_two_three", + ATTR_ENTITY_PICTURE: "blabla.png", + ATTR_FRIENDLY_NAME: "REST Switch", + ATTR_ICON: "mdi:one_two_three", } From f479b2385efd7b88d9d593b3102de76580dec3ab Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 7 Nov 2022 14:02:49 +0100 Subject: [PATCH 276/394] Add type hints to rest switch (#81307) --- homeassistant/components/rest/switch.py | 52 ++++++++++++------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 7e470674b1ee5d..cda35d1f918aaf 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -81,8 +81,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the RESTful switch.""" - resource = config.get(CONF_RESOURCE) - unique_id = config.get(CONF_UNIQUE_ID) + resource: str = config[CONF_RESOURCE] + unique_id: str | None = config.get(CONF_UNIQUE_ID) try: switch = RestSwitch(hass, config, unique_id) @@ -106,10 +106,10 @@ class RestSwitch(TemplateEntity, SwitchEntity): def __init__( self, - hass, - config, - unique_id, - ): + hass: HomeAssistant, + config: ConfigType, + unique_id: str | None, + ) -> None: """Initialize the REST switch.""" TemplateEntity.__init__( self, @@ -119,30 +119,30 @@ def __init__( unique_id=unique_id, ) - auth = None + auth: aiohttp.BasicAuth | None = None + username: str | None = None if username := config.get(CONF_USERNAME): - auth = aiohttp.BasicAuth(username, password=config[CONF_PASSWORD]) - - self._resource = config.get(CONF_RESOURCE) - self._state_resource = config.get(CONF_STATE_RESOURCE) or self._resource - self._method = config.get(CONF_METHOD) - self._headers = config.get(CONF_HEADERS) - self._params = config.get(CONF_PARAMS) + password: str = config[CONF_PASSWORD] + auth = aiohttp.BasicAuth(username, password=password) + + self._resource: str = config[CONF_RESOURCE] + self._state_resource: str = config.get(CONF_STATE_RESOURCE) or self._resource + self._method: str = config[CONF_METHOD] + self._headers: dict[str, template.Template] | None = config.get(CONF_HEADERS) + self._params: dict[str, template.Template] | None = config.get(CONF_PARAMS) self._auth = auth - self._body_on = config.get(CONF_BODY_ON) - self._body_off = config.get(CONF_BODY_OFF) - self._is_on_template = config.get(CONF_IS_ON_TEMPLATE) - self._timeout = config.get(CONF_TIMEOUT) - self._verify_ssl = config.get(CONF_VERIFY_SSL) + self._body_on: template.Template = config[CONF_BODY_ON] + self._body_off: template.Template = config[CONF_BODY_OFF] + self._is_on_template: template.Template | None = config.get(CONF_IS_ON_TEMPLATE) + self._timeout: int = config[CONF_TIMEOUT] + self._verify_ssl: bool = config[CONF_VERIFY_SSL] self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._body_on.hass = hass + self._body_off.hass = hass if (is_on_template := self._is_on_template) is not None: is_on_template.hass = hass - if (body_on := self._body_on) is not None: - body_on.hass = hass - if (body_off := self._body_off) is not None: - body_off.hass = hass template.attach(hass, self._headers) template.attach(hass, self._params) @@ -178,7 +178,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Error while switching off %s", self._resource) - async def set_device_state(self, body): + async def set_device_state(self, body: Any) -> aiohttp.ClientResponse: """Send a state update to the device.""" websession = async_get_clientsession(self.hass, self._verify_ssl) @@ -186,7 +186,7 @@ async def set_device_state(self, body): rendered_params = template.render_complex(self._params) async with async_timeout.timeout(self._timeout): - req = await getattr(websession, self._method)( + req: aiohttp.ClientResponse = await getattr(websession, self._method)( self._resource, auth=self._auth, data=bytes(body, "utf-8"), @@ -204,7 +204,7 @@ async def async_update(self) -> None: except aiohttp.ClientError as err: _LOGGER.exception("Error while fetching data: %s", err) - async def get_device_state(self, hass): + async def get_device_state(self, hass: HomeAssistant) -> aiohttp.ClientResponse: """Get the latest data from REST API and update the state.""" websession = async_get_clientsession(hass, self._verify_ssl) From 902e075d5880b6fec559239950ee4f12133f400a Mon Sep 17 00:00:00 2001 From: StefanIacobLivisi <109964424+StefanIacobLivisi@users.noreply.github.com> Date: Mon, 7 Nov 2022 15:40:23 +0200 Subject: [PATCH 277/394] Add livisi integration (#76863) --- CODEOWNERS | 2 + homeassistant/components/livisi/__init__.py | 57 +++++++ .../components/livisi/config_flow.py | 88 ++++++++++ homeassistant/components/livisi/const.py | 18 ++ .../components/livisi/coordinator.py | 132 ++++++++++++++ homeassistant/components/livisi/manifest.json | 9 + homeassistant/components/livisi/strings.json | 18 ++ homeassistant/components/livisi/switch.py | 161 ++++++++++++++++++ .../components/livisi/translations/en.json | 18 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/livisi/__init__.py | 37 ++++ tests/components/livisi/test_config_flow.py | 68 ++++++++ 15 files changed, 621 insertions(+) create mode 100644 homeassistant/components/livisi/__init__.py create mode 100644 homeassistant/components/livisi/config_flow.py create mode 100644 homeassistant/components/livisi/const.py create mode 100644 homeassistant/components/livisi/coordinator.py create mode 100644 homeassistant/components/livisi/manifest.json create mode 100644 homeassistant/components/livisi/strings.json create mode 100644 homeassistant/components/livisi/switch.py create mode 100644 homeassistant/components/livisi/translations/en.json create mode 100644 tests/components/livisi/__init__.py create mode 100644 tests/components/livisi/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index d0e4fc993ad477..d33cf5450386dc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -635,6 +635,8 @@ build.json @home-assistant/supervisor /tests/components/litejet/ @joncar /homeassistant/components/litterrobot/ @natekspencer @tkdrob /tests/components/litterrobot/ @natekspencer @tkdrob +/homeassistant/components/livisi/ @StefanIacobLivisi +/tests/components/livisi/ @StefanIacobLivisi /homeassistant/components/local_ip/ @issacg /tests/components/local_ip/ @issacg /homeassistant/components/lock/ @home-assistant/core diff --git a/homeassistant/components/livisi/__init__.py b/homeassistant/components/livisi/__init__.py new file mode 100644 index 00000000000000..38e0c9f8e7ddc5 --- /dev/null +++ b/homeassistant/components/livisi/__init__.py @@ -0,0 +1,57 @@ +"""The Livisi Smart Home integration.""" +from __future__ import annotations + +import asyncio +from typing import Final + +from aiohttp import ClientConnectorError +from aiolivisi import AioLivisi + +from homeassistant import core +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, device_registry as dr + +from .const import DOMAIN, SWITCH_PLATFORM +from .coordinator import LivisiDataUpdateCoordinator + +PLATFORMS: Final = [SWITCH_PLATFORM] + + +async def async_setup_entry(hass: core.HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Livisi Smart Home from a config entry.""" + web_session = aiohttp_client.async_get_clientsession(hass) + aiolivisi = AioLivisi(web_session) + coordinator = LivisiDataUpdateCoordinator(hass, entry, aiolivisi) + try: + await coordinator.async_setup() + await coordinator.async_set_all_rooms() + except ClientConnectorError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=coordinator.serial_number, + identifiers={(DOMAIN, entry.entry_id)}, + manufacturer="Livisi", + name=f"SHC {coordinator.controller_type} {coordinator.serial_number}", + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await coordinator.async_config_entry_first_refresh() + asyncio.create_task(coordinator.ws_connect()) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + unload_success = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + await coordinator.websocket.disconnect() + if unload_success: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_success diff --git a/homeassistant/components/livisi/config_flow.py b/homeassistant/components/livisi/config_flow.py new file mode 100644 index 00000000000000..16cccaacfd113f --- /dev/null +++ b/homeassistant/components/livisi/config_flow.py @@ -0,0 +1,88 @@ +"""Config flow for Livisi Home Assistant.""" +from __future__ import annotations + +from contextlib import suppress +from typing import Any + +from aiohttp import ClientConnectorError +from aiolivisi import AioLivisi, errors as livisi_errors +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client + +from .const import CONF_HOST, CONF_PASSWORD, DOMAIN, LOGGER + + +class LivisiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Livisi Smart Home config flow.""" + + def __init__(self) -> None: + """Create the configuration file.""" + self.aio_livisi: AioLivisi = None + self.data_schema = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=self.data_schema) + + errors = {} + try: + await self._login(user_input) + except livisi_errors.WrongCredentialException: + errors["base"] = "wrong_password" + except livisi_errors.ShcUnreachableException: + errors["base"] = "cannot_connect" + except livisi_errors.IncorrectIpAddressException: + errors["base"] = "wrong_ip_address" + else: + controller_info: dict[str, Any] = {} + with suppress(ClientConnectorError): + controller_info = await self.aio_livisi.async_get_controller() + if controller_info: + return await self.create_entity(user_input, controller_info) + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", data_schema=self.data_schema, errors=errors + ) + + async def _login(self, user_input: dict[str, str]) -> None: + """Login into Livisi Smart Home.""" + web_session = aiohttp_client.async_get_clientsession(self.hass) + self.aio_livisi = AioLivisi(web_session) + livisi_connection_data = { + "ip_address": user_input[CONF_HOST], + "password": user_input[CONF_PASSWORD], + } + + await self.aio_livisi.async_set_token(livisi_connection_data) + + async def create_entity( + self, user_input: dict[str, str], controller_info: dict[str, Any] + ) -> FlowResult: + """Create LIVISI entity.""" + if (controller_data := controller_info.get("gateway")) is None: + controller_data = controller_info + controller_type = controller_data["controllerType"] + LOGGER.debug( + "Integrating SHC %s with serial number: %s", + controller_type, + controller_data["serialNumber"], + ) + + return self.async_create_entry( + title=f"SHC {controller_type}", + data={ + **user_input, + }, + ) diff --git a/homeassistant/components/livisi/const.py b/homeassistant/components/livisi/const.py new file mode 100644 index 00000000000000..e6abc5118deab0 --- /dev/null +++ b/homeassistant/components/livisi/const.py @@ -0,0 +1,18 @@ +"""Constants for the Livisi Smart Home integration.""" +import logging +from typing import Final + +LOGGER = logging.getLogger(__package__) +DOMAIN = "livisi" + +CONF_HOST = "host" +CONF_PASSWORD: Final = "password" +AVATAR_PORT: Final = 9090 +CLASSIC_PORT: Final = 8080 +DEVICE_POLLING_DELAY: Final = 60 +LIVISI_STATE_CHANGE: Final = "livisi_state_change" +LIVISI_REACHABILITY_CHANGE: Final = "livisi_reachability_change" + +SWITCH_PLATFORM: Final = "switch" + +PSS_DEVICE_TYPE: Final = "PSS" diff --git a/homeassistant/components/livisi/coordinator.py b/homeassistant/components/livisi/coordinator.py new file mode 100644 index 00000000000000..70640c260fb155 --- /dev/null +++ b/homeassistant/components/livisi/coordinator.py @@ -0,0 +1,132 @@ +"""Code to manage fetching LIVISI data API.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from aiohttp import ClientConnectorError +from aiolivisi import AioLivisi, LivisiEvent, Websocket + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + AVATAR_PORT, + CLASSIC_PORT, + CONF_HOST, + CONF_PASSWORD, + DEVICE_POLLING_DELAY, + LIVISI_REACHABILITY_CHANGE, + LIVISI_STATE_CHANGE, + LOGGER, +) + + +class LivisiDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): + """Class to manage fetching LIVISI data API.""" + + config_entry: ConfigEntry + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, aiolivisi: AioLivisi + ) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + LOGGER, + name="Livisi devices", + update_interval=timedelta(seconds=DEVICE_POLLING_DELAY), + ) + self.config_entry = config_entry + self.hass = hass + self.aiolivisi = aiolivisi + self.websocket = Websocket(aiolivisi) + self.devices: set[str] = set() + self.rooms: dict[str, Any] = {} + self.serial_number: str = "" + self.controller_type: str = "" + self.is_avatar: bool = False + self.port: int = 0 + + async def _async_update_data(self) -> list[dict[str, Any]]: + """Get device configuration from LIVISI.""" + try: + return await self.async_get_devices() + except ClientConnectorError as exc: + raise UpdateFailed("Failed to get LIVISI the devices") from exc + + async def async_setup(self) -> None: + """Set up the Livisi Smart Home Controller.""" + if not self.aiolivisi.livisi_connection_data: + livisi_connection_data = { + "ip_address": self.config_entry.data[CONF_HOST], + "password": self.config_entry.data[CONF_PASSWORD], + } + + await self.aiolivisi.async_set_token( + livisi_connection_data=livisi_connection_data + ) + controller_data = await self.aiolivisi.async_get_controller() + if controller_data["controllerType"] == "Avatar": + self.port = AVATAR_PORT + self.is_avatar = True + else: + self.port = CLASSIC_PORT + self.is_avatar = False + self.serial_number = controller_data["serialNumber"] + self.controller_type = controller_data["controllerType"] + + async def async_get_devices(self) -> list[dict[str, Any]]: + """Set the discovered devices list.""" + return await self.aiolivisi.async_get_devices() + + async def async_get_pss_state(self, capability: str) -> bool | None: + """Set the PSS state.""" + response: dict[str, Any] = await self.aiolivisi.async_get_pss_state( + capability[1:] + ) + if response is None: + return None + on_state = response["onState"] + return on_state["value"] + + async def async_set_all_rooms(self) -> None: + """Set the room list.""" + response: list[dict[str, Any]] = await self.aiolivisi.async_get_all_rooms() + + for available_room in response: + available_room_config: dict[str, Any] = available_room["config"] + self.rooms[available_room["id"]] = available_room_config["name"] + + def on_data(self, event_data: LivisiEvent) -> None: + """Define a handler to fire when the data is received.""" + if event_data.onState is not None: + async_dispatcher_send( + self.hass, + f"{LIVISI_STATE_CHANGE}_{event_data.source}", + event_data.onState, + ) + if event_data.isReachable is not None: + async_dispatcher_send( + self.hass, + f"{LIVISI_REACHABILITY_CHANGE}_{event_data.source}", + event_data.isReachable, + ) + + async def on_close(self) -> None: + """Define a handler to fire when the websocket is closed.""" + for device_id in self.devices: + is_reachable: bool = False + async_dispatcher_send( + self.hass, + f"{LIVISI_REACHABILITY_CHANGE}_{device_id}", + is_reachable, + ) + + await self.websocket.connect(self.on_data, self.on_close, self.port) + + async def ws_connect(self) -> None: + """Connect the websocket.""" + await self.websocket.connect(self.on_data, self.on_close, self.port) diff --git a/homeassistant/components/livisi/manifest.json b/homeassistant/components/livisi/manifest.json new file mode 100644 index 00000000000000..83045d9eb60275 --- /dev/null +++ b/homeassistant/components/livisi/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "livisi", + "name": "LIVISI Smart Home", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/livisi", + "requirements": ["aiolivisi==0.0.14"], + "codeowners": ["@StefanIacobLivisi"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/livisi/strings.json b/homeassistant/components/livisi/strings.json new file mode 100644 index 00000000000000..260ef07234ba88 --- /dev/null +++ b/homeassistant/components/livisi/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "description": "Enter the IP address and the (local) password of the SHC.", + "data": { + "host": "[%key:common::config_flow::data::ip%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "wrong_password": "The password is incorrect.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "wrong_ip_address": "The IP address is incorrect or the SHC cannot be reached locally." + } + } +} diff --git a/homeassistant/components/livisi/switch.py b/homeassistant/components/livisi/switch.py new file mode 100644 index 00000000000000..bcb9a2044119ad --- /dev/null +++ b/homeassistant/components/livisi/switch.py @@ -0,0 +1,161 @@ +"""Code to handle a Livisi switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + DOMAIN, + LIVISI_REACHABILITY_CHANGE, + LIVISI_STATE_CHANGE, + LOGGER, + PSS_DEVICE_TYPE, +) +from .coordinator import LivisiDataUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switch device.""" + coordinator: LivisiDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + @callback + def handle_coordinator_update() -> None: + """Add switch.""" + shc_devices: list[dict[str, Any]] = coordinator.data + entities: list[SwitchEntity] = [] + for device in shc_devices: + if ( + device["type"] == PSS_DEVICE_TYPE + and device["id"] not in coordinator.devices + ): + livisi_switch: SwitchEntity = create_entity( + config_entry, device, coordinator + ) + LOGGER.debug("Include device type: %s", device["type"]) + coordinator.devices.add(device["id"]) + entities.append(livisi_switch) + async_add_entities(entities) + + config_entry.async_on_unload( + coordinator.async_add_listener(handle_coordinator_update) + ) + + +def create_entity( + config_entry: ConfigEntry, + device: dict[str, Any], + coordinator: LivisiDataUpdateCoordinator, +) -> SwitchEntity: + """Create Switch Entity.""" + config_details: dict[str, Any] = device["config"] + capabilities: list = device["capabilities"] + room_id: str = device["location"] + room_name: str = coordinator.rooms[room_id] + livisi_switch = LivisiSwitch( + config_entry, + coordinator, + unique_id=device["id"], + manufacturer=device["manufacturer"], + device_type=device["type"], + name=config_details["name"], + capability_id=capabilities[0], + room=room_name, + ) + return livisi_switch + + +class LivisiSwitch(CoordinatorEntity[LivisiDataUpdateCoordinator], SwitchEntity): + """Represents the Livisi Switch.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: LivisiDataUpdateCoordinator, + unique_id: str, + manufacturer: str, + device_type: str, + name: str, + capability_id: str, + room: str, + ) -> None: + """Initialize the Livisi Switch.""" + self.config_entry = config_entry + self._attr_unique_id = unique_id + self._attr_name = name + self._capability_id = capability_id + self.aio_livisi = coordinator.aiolivisi + self._attr_available = False + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=manufacturer, + model=device_type, + name=name, + suggested_area=room, + via_device=(DOMAIN, config_entry.entry_id), + ) + super().__init__(coordinator) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + response = await self.aio_livisi.async_pss_set_state( + self._capability_id, is_on=True + ) + if response is None: + self._attr_available = False + raise HomeAssistantError(f"Failed to turn on {self._attr_name}") + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + response = await self.aio_livisi.async_pss_set_state( + self._capability_id, is_on=False + ) + if response is None: + self._attr_available = False + raise HomeAssistantError(f"Failed to turn off {self._attr_name}") + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + response = await self.coordinator.async_get_pss_state(self._capability_id) + if response is None: + self._attr_is_on = False + self._attr_available = False + else: + self._attr_is_on = response + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{LIVISI_STATE_CHANGE}_{self._capability_id}", + self.update_states, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{LIVISI_REACHABILITY_CHANGE}_{self.unique_id}", + self.update_reachability, + ) + ) + + @callback + def update_states(self, state: bool) -> None: + """Update the states of the switch device.""" + self._attr_is_on = state + self.async_write_ha_state() + + @callback + def update_reachability(self, is_reachable: bool) -> None: + """Update the reachability of the switch device.""" + self._attr_available = is_reachable + self.async_write_ha_state() diff --git a/homeassistant/components/livisi/translations/en.json b/homeassistant/components/livisi/translations/en.json new file mode 100644 index 00000000000000..d561f09dd061e7 --- /dev/null +++ b/homeassistant/components/livisi/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "wrong_ip_address": "The IP address is incorrect or the SHC cannot be reached locally.", + "wrong_password": "The password is incorrect." + }, + "step": { + "user": { + "data": { + "host": "IP Address", + "password": "Password" + }, + "description": "Enter the IP address and the (local) password of the SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b0cd687b54e13b..22dd4f491e08c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -215,6 +215,7 @@ "lifx", "litejet", "litterrobot", + "livisi", "local_ip", "locative", "logi_circle", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 89de4fa92fca43..d033c262fbdc25 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2821,6 +2821,12 @@ "config_flow": true, "iot_class": "cloud_push" }, + "livisi": { + "name": "LIVISI Smart Home", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "llamalab_automate": { "name": "LlamaLab Automate", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 0b1541dd0771b1..b60e9e1792e4a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -201,6 +201,9 @@ aiolifx_effects==0.3.0 # homeassistant.components.lifx aiolifx_themes==0.2.0 +# homeassistant.components.livisi +aiolivisi==0.0.14 + # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7061345c0f3f98..f485e7453d56e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,6 +179,9 @@ aiolifx_effects==0.3.0 # homeassistant.components.lifx aiolifx_themes==0.2.0 +# homeassistant.components.livisi +aiolivisi==0.0.14 + # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/tests/components/livisi/__init__.py b/tests/components/livisi/__init__.py new file mode 100644 index 00000000000000..3d28d1db70801f --- /dev/null +++ b/tests/components/livisi/__init__.py @@ -0,0 +1,37 @@ +"""Tests for the LIVISI Smart Home integration.""" +from unittest.mock import patch + +from homeassistant.components.livisi.const import CONF_HOST, CONF_PASSWORD + +VALID_CONFIG = { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test", +} + +DEVICE_CONFIG = { + "serialNumber": "1234", + "controllerType": "Classic", +} + + +def mocked_livisi_login(): + """Create mock for LIVISI login.""" + return patch( + "homeassistant.components.livisi.config_flow.AioLivisi.async_set_token" + ) + + +def mocked_livisi_controller(): + """Create mock data for LIVISI controller.""" + return patch( + "homeassistant.components.livisi.config_flow.AioLivisi.async_get_controller", + return_value=DEVICE_CONFIG, + ) + + +def mocked_livisi_setup_entry(): + """Create mock for LIVISI setup entry.""" + return patch( + "homeassistant.components.livisi.async_setup_entry", + return_value=True, + ) diff --git a/tests/components/livisi/test_config_flow.py b/tests/components/livisi/test_config_flow.py new file mode 100644 index 00000000000000..c9924d39b9b125 --- /dev/null +++ b/tests/components/livisi/test_config_flow.py @@ -0,0 +1,68 @@ +"""Test the Livisi Home Assistant config flow.""" + +from unittest.mock import patch + +from aiolivisi import errors as livisi_errors +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.livisi.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER + +from . import ( + VALID_CONFIG, + mocked_livisi_controller, + mocked_livisi_login, + mocked_livisi_setup_entry, +) + + +async def test_create_entry(hass): + """Test create LIVISI entity.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with mocked_livisi_login(), mocked_livisi_controller(), mocked_livisi_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "SHC Classic" + assert result["data"]["host"] == "1.1.1.1" + assert result["data"]["password"] == "test" + + +@pytest.mark.parametrize( + "exception,expected_reason", + [ + (livisi_errors.ShcUnreachableException(), "cannot_connect"), + (livisi_errors.IncorrectIpAddressException(), "wrong_ip_address"), + (livisi_errors.WrongCredentialException(), "wrong_password"), + ], +) +async def test_create_entity_after_login_error( + hass, exception: livisi_errors.LivisiException, expected_reason: str +): + """Test the LIVISI integration can create an entity after the user had login errors.""" + with patch( + "homeassistant.components.livisi.config_flow.AioLivisi.async_set_token", + side_effect=exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], VALID_CONFIG + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"]["base"] == expected_reason + with mocked_livisi_login(), mocked_livisi_controller(), mocked_livisi_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=VALID_CONFIG, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY From 523c3089f7aaab51edaad157989b109d97b88095 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 7 Nov 2022 14:54:43 +0100 Subject: [PATCH 278/394] Add TI router transmit power config entity to ZHA (#81520) Make TI Router Transmit Power configurable in ZHA --- .../components/zha/core/channels/general.py | 6 ++++++ homeassistant/components/zha/number.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index de68ed7d8ef11b..3756e3c1233f26 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -161,6 +161,12 @@ def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: self.ZCL_INIT_ATTRS.copy() ) self.ZCL_INIT_ATTRS["trigger_indicator"] = True + elif ( + self.cluster.endpoint.manufacturer == "TexasInstruments" + and self.cluster.endpoint.model == "ti.router" + ): + self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() + self.ZCL_INIT_ATTRS["transmit_power"] = True @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 1776cabf125682..853ab189bbfb96 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -19,6 +19,7 @@ from .core import discovery from .core.const import ( CHANNEL_ANALOG_OUTPUT, + CHANNEL_BASIC, CHANNEL_COLOR, CHANNEL_INOVELLI, CHANNEL_LEVEL, @@ -585,6 +586,20 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time") _attr_name = "Filter life time" +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_BASIC, + manufacturers={"TexasInstruments"}, + models={"ti.router"}, +) +class TiRouterTransmitPower(ZHANumberConfigurationEntity, id_suffix="transmit_power"): + """Representation of a ZHA TI transmit power configuration entity.""" + + _attr_native_min_value: float = -20 + _attr_native_max_value: float = 20 + _zcl_attribute: str = "transmit_power" + _attr_name = "Transmit power" + + @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) class InovelliRemoteDimmingUpSpeed( ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_remote" From ff18cece7b7d87eeeda4a72ffac4fdd624bc6ca4 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Mon, 7 Nov 2022 16:17:56 +0100 Subject: [PATCH 279/394] Add Brandt, Hexaom, SIMU & Ubiwizz as virtuals integrations of Overkiz (#79566) * Match supported brands with Overkiz libs * Add missing files for mypy --- homeassistant/components/brandt/__init__.py | 1 + homeassistant/components/brandt/manifest.json | 6 ++++++ homeassistant/components/hexaom/__init__.py | 1 + homeassistant/components/hexaom/manifest.json | 6 ++++++ homeassistant/components/simu/__init__.py | 1 + homeassistant/components/simu/manifest.json | 6 ++++++ homeassistant/components/ubiwizz/__init__.py | 1 + .../components/ubiwizz/manifest.json | 6 ++++++ homeassistant/generated/integrations.json | 20 +++++++++++++++++++ 9 files changed, 48 insertions(+) create mode 100644 homeassistant/components/brandt/__init__.py create mode 100644 homeassistant/components/brandt/manifest.json create mode 100644 homeassistant/components/hexaom/__init__.py create mode 100644 homeassistant/components/hexaom/manifest.json create mode 100644 homeassistant/components/simu/__init__.py create mode 100644 homeassistant/components/simu/manifest.json create mode 100644 homeassistant/components/ubiwizz/__init__.py create mode 100644 homeassistant/components/ubiwizz/manifest.json diff --git a/homeassistant/components/brandt/__init__.py b/homeassistant/components/brandt/__init__.py new file mode 100644 index 00000000000000..d2c9435c863129 --- /dev/null +++ b/homeassistant/components/brandt/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Brandt Smart Control.""" diff --git a/homeassistant/components/brandt/manifest.json b/homeassistant/components/brandt/manifest.json new file mode 100644 index 00000000000000..4fab6b2ec16866 --- /dev/null +++ b/homeassistant/components/brandt/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "brandt", + "name": "Brandt Smart Control", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/components/hexaom/__init__.py b/homeassistant/components/hexaom/__init__.py new file mode 100644 index 00000000000000..9b46a4f0e1cfda --- /dev/null +++ b/homeassistant/components/hexaom/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Hexaom Hexaconnect.""" diff --git a/homeassistant/components/hexaom/manifest.json b/homeassistant/components/hexaom/manifest.json new file mode 100644 index 00000000000000..738ffdb4fbfd2e --- /dev/null +++ b/homeassistant/components/hexaom/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "hexaom", + "name": "Hexaom Hexaconnect", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/components/simu/__init__.py b/homeassistant/components/simu/__init__.py new file mode 100644 index 00000000000000..7b02a1109f747d --- /dev/null +++ b/homeassistant/components/simu/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: SIMU LiveIn2.""" diff --git a/homeassistant/components/simu/manifest.json b/homeassistant/components/simu/manifest.json new file mode 100644 index 00000000000000..a1cf964f438022 --- /dev/null +++ b/homeassistant/components/simu/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "simu", + "name": "SIMU LiveIn2", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/components/ubiwizz/__init__.py b/homeassistant/components/ubiwizz/__init__.py new file mode 100644 index 00000000000000..0126a15b983e97 --- /dev/null +++ b/homeassistant/components/ubiwizz/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Ubiwizz.""" diff --git a/homeassistant/components/ubiwizz/manifest.json b/homeassistant/components/ubiwizz/manifest.json new file mode 100644 index 00000000000000..a6b5d6e7317b36 --- /dev/null +++ b/homeassistant/components/ubiwizz/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "ubiwizz", + "name": "Ubiwizz", + "integration_type": "virtual", + "supported_by": "overkiz" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index d033c262fbdc25..69e7d22ba828d6 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -608,6 +608,11 @@ "config_flow": true, "iot_class": "local_push" }, + "brandt": { + "name": "Brandt Smart Control", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "brel_home": { "name": "Brel Home", "integration_type": "virtual", @@ -2121,6 +2126,11 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "hexaom": { + "name": "Hexaom Hexaconnect", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "hi_kumo": { "name": "Hitachi Hi Kumo", "integration_type": "virtual", @@ -4699,6 +4709,11 @@ "integration_type": "virtual", "supported_by": "upb" }, + "simu": { + "name": "SIMU LiveIn2", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "simulated": { "name": "Simulated", "integration_type": "hub", @@ -5607,6 +5622,11 @@ } } }, + "ubiwizz": { + "name": "Ubiwizz", + "integration_type": "virtual", + "supported_by": "overkiz" + }, "uk_transport": { "name": "UK Transport", "integration_type": "hub", From 43745dbc6c86f14993575449deda15f8bae4df4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 09:26:38 -0600 Subject: [PATCH 280/394] Pass explict time in logbook tests (#81725) The CI sets the timezone to US/Pacific and the logbook uses start_of_local_day when called without a time. We now call the logbook api with a specific time to avoid them being out of sync since the test would fail at CET 8:55am on Mon Nov 7th 2022 (and probably other dates) --- tests/components/logbook/test_init.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index fb7ac217867683..366b4b30ed5431 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -489,7 +489,15 @@ def _describe(event): await async_wait_recording_done(hass) client = await hass_client() - response = await client.get("/api/logbook") + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) results = await response.json() assert len(results) == 1 event = results[0] @@ -553,7 +561,15 @@ def async_describe_events(hass, async_describe_event): await async_wait_recording_done(hass) client = await hass_client() - response = await client.get("/api/logbook") + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) results = await response.json() assert len(results) == 1 event = results[0] From 3788a950e62fe3284ecf74d12bd61dcac933bce1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 7 Nov 2022 11:14:57 -0500 Subject: [PATCH 281/394] Validate matcher field case in `usb.async_is_plugged_in` (#81514) * Support case-insensitive matching * Revert "Support case-insensitive matching" This reverts commit 0fdb2aa6bc6165d9adae39ecbe7f6698e7b94715. * Explicitly check the case of matcher fields in `async_is_plugged_in` --- homeassistant/components/usb/__init__.py | 18 +++++++++++++++ tests/components/usb/test_init.py | 29 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 7c0355fa24cb3b..0f81d2e42d6714 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -64,6 +64,24 @@ def async_register_scan_request_callback( @hass_callback def async_is_plugged_in(hass: HomeAssistant, matcher: USBCallbackMatcher) -> bool: """Return True is a USB device is present.""" + + vid = matcher.get("vid", "") + pid = matcher.get("pid", "") + serial_number = matcher.get("serial_number", "") + manufacturer = matcher.get("manufacturer", "") + description = matcher.get("description", "") + + if ( + vid != vid.upper() + or pid != pid.upper() + or serial_number != serial_number.lower() + or manufacturer != manufacturer.lower() + or description != description.lower() + ): + raise ValueError( + f"vid and pid must be uppercase, the rest lowercase in matcher {matcher!r}" + ) + usb_discovery: USBDiscovery = hass.data[DOMAIN] return any( _is_matching(USBDevice(*device_tuple), matcher) diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index ca978af75f2ad9..c7196fed0c522c 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -877,6 +877,35 @@ async def test_async_is_plugged_in(hass, hass_ws_client): assert usb.async_is_plugged_in(hass, matcher) +@pytest.mark.parametrize( + "matcher", + [ + {"vid": "abcd"}, + {"pid": "123a"}, + {"serial_number": "1234ABCD"}, + {"manufacturer": "Some Manufacturer"}, + {"description": "A description"}, + ], +) +async def test_async_is_plugged_in_case_enforcement(hass, matcher): + """Test `async_is_plugged_in` throws an error when incorrect cases are used.""" + + new_usb = [{"domain": "test1", "vid": "ABCD"}] + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=new_usb + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + usb.async_is_plugged_in(hass, matcher) + + async def test_web_socket_triggers_discovery_request_callbacks(hass, hass_ws_client): """Test the websocket call triggers a discovery request callback.""" mock_callback = Mock() From 604cd46ec90266f188fe25c6f46aa555a5e016d2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 7 Nov 2022 11:26:16 -0500 Subject: [PATCH 282/394] Enable SkyConnect config flow and use correct case in USB matching (#81522) * Ensure `USBCallbackMatcher` uses the appropriate case for each field * Enable the config flow for the SkyConnect integration * Update unit test --- .../homeassistant_sky_connect/__init__.py | 20 ++++++++++++---- .../homeassistant_sky_connect/manifest.json | 2 +- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/usb.py | 6 +++++ .../homeassistant_sky_connect/test_init.py | 23 ++++++++++++------- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index dd4cf013fab61c..cb00f0e32ec8a5 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -1,16 +1,29 @@ """The Home Assistant Sky Connect integration.""" from __future__ import annotations -from typing import cast - from homeassistant.components import usb from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from .const import DOMAIN + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a Home Assistant Sky Connect config entry.""" + matcher = usb.USBCallbackMatcher( + domain=DOMAIN, + vid=entry.data["vid"].upper(), + pid=entry.data["pid"].upper(), + serial_number=entry.data["serial_number"].lower(), + manufacturer=entry.data["manufacturer"].lower(), + description=entry.data["description"].lower(), + ) + + if not usb.async_is_plugged_in(hass, matcher): + # The USB dongle is not plugged in + raise ConfigEntryNotReady + usb_info = usb.UsbServiceInfo( device=entry.data["device"], vid=entry.data["vid"], @@ -19,9 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer=entry.data["manufacturer"], description=entry.data["description"], ) - if not usb.async_is_plugged_in(hass, cast(usb.USBCallbackMatcher, entry.data)): - # The USB dongle is not plugged in - raise ConfigEntryNotReady await hass.config_entries.flow.async_init( "zha", diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index 5ccb8bd53312dc..8fdc1d3c1d13c5 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -1,7 +1,7 @@ { "domain": "homeassistant_sky_connect", "name": "Home Assistant Sky Connect", - "config_flow": false, + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", "dependencies": ["hardware", "usb"], "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 22dd4f491e08c6..05e352ab2b7840 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -161,6 +161,7 @@ "hlk_sw16", "home_connect", "home_plus_control", + "homeassistant_sky_connect", "homekit", "homekit_controller", "homematicip_cloud", diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 901f9f72da586b..59b59bb7604433 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -4,6 +4,12 @@ """ USB = [ + { + "domain": "homeassistant_sky_connect", + "vid": "10C4", + "pid": "EA60", + "description": "*skyconnect v1.0*", + }, { "domain": "insteon", "vid": "10BF", diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index 05b883a9726f69..179a0eff8dabbc 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -13,12 +13,12 @@ from tests.common import MockConfigEntry CONFIG_ENTRY_DATA = { - "device": "bla_device", - "vid": "bla_vid", - "pid": "bla_pid", - "serial_number": "bla_serial_number", - "manufacturer": "bla_manufacturer", - "description": "bla_description", + "device": "/dev/serial/by-id/usb-Nabu_Casa_SkyConnect_v1.0_9e2adbd75b8beb119fe564a0f320645d-if00-port0", + "vid": "10C4", + "pid": "EA60", + "serial_number": "3c0ed67c628beb11b1cd64a0f320645d", + "manufacturer": "Nabu Casa", + "description": "SkyConnect v1.0", } @@ -67,6 +67,13 @@ async def test_setup_entry( await hass.async_block_till_done() assert len(mock_is_plugged_in.mock_calls) == 1 + matcher = mock_is_plugged_in.mock_calls[0].args[1] + assert matcher["vid"].isupper() + assert matcher["pid"].isupper() + assert matcher["serial_number"].islower() + assert matcher["manufacturer"].islower() + assert matcher["description"].islower() + # Finish setting up ZHA if num_entries > 0: zha_flows = hass.config_entries.flow.async_progress_by_handler("zha") @@ -119,12 +126,12 @@ async def test_setup_zha(mock_zha_config_flow_setup, hass: HomeAssistant) -> Non "device": { "baudrate": 115200, "flow_control": "software", - "path": "bla_device", + "path": CONFIG_ENTRY_DATA["device"], }, "radio_type": "ezsp", } assert config_entry.options == {} - assert config_entry.title == "bla_description" + assert config_entry.title == CONFIG_ENTRY_DATA["description"] async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: From 74357bef15f0e58fe6a2404e0510298062a12a2d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 7 Nov 2022 11:28:28 -0500 Subject: [PATCH 283/394] Use a unique ID for the Yellow ZHA hardware discovery (#81523) * Set unique ID for hardware discovery * Use the provided `name` for discovered hardware --- homeassistant/components/zha/config_flow.py | 100 ++++++++++++-------- tests/components/zha/test_config_flow.py | 1 + 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 85f03b9f1f56d3..be422778e2d737 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -76,6 +76,14 @@ CONNECT_DELAY_S = 1.0 +HARDWARE_DISCOVERY_SCHEMA = vol.Schema( + { + vol.Required("name"): str, + vol.Required("port"): dict, + vol.Required("radio_type"): str, + } +) + _LOGGER = logging.getLogger(__name__) @@ -182,6 +190,13 @@ async def _restore_backup( async with self._connect_zigpy_app() as app: await app.backups.restore_backup(backup, **kwargs) + def _parse_radio_type(self, radio_type: str) -> RadioType: + """Parse a radio type name, accounting for past aliases.""" + if radio_type == "efr32": + return RadioType.ezsp + + return RadioType[radio_type] + async def _detect_radio_type(self) -> bool: """Probe all radio types on the current port.""" for radio in AUTOPROBE_RADIOS: @@ -544,6 +559,24 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN VERSION = 3 + async def _set_unique_id_or_update_path( + self, unique_id: str, device_path: str + ) -> None: + """Set the flow's unique ID and update the device path if it isn't unique.""" + current_entry = await self.async_set_unique_id(unique_id) + + if not current_entry: + return + + self._abort_if_unique_id_configured( + updates={ + CONF_DEVICE: { + **current_entry.data.get(CONF_DEVICE, {}), + CONF_DEVICE_PATH: device_path, + }, + } + ) + @staticmethod @callback def async_get_options_flow( @@ -600,16 +633,11 @@ async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult manufacturer = discovery_info.manufacturer description = discovery_info.description dev_path = await self.hass.async_add_executor_job(usb.get_serial_by_id, device) - unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" - if current_entry := await self.async_set_unique_id(unique_id): - self._abort_if_unique_id_configured( - updates={ - CONF_DEVICE: { - **current_entry.data.get(CONF_DEVICE, {}), - CONF_DEVICE_PATH: dev_path, - }, - } - ) + + await self._set_unique_id_or_update_path( + unique_id=f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}", + device_path=dev_path, + ) # If they already have a discovery for deconz we ignore the usb discovery as # they probably want to use it there instead @@ -645,7 +673,9 @@ async def async_step_zeroconf( port = DEFAULT_ZHA_ZEROCONF_PORT if "radio_type" in discovery_info.properties: - self._radio_type = RadioType[discovery_info.properties["radio_type"]] + self._radio_type = self._parse_radio_type( + discovery_info.properties["radio_type"] + ) elif "efr32" in local_name: self._radio_type = RadioType.ezsp else: @@ -654,15 +684,10 @@ async def async_step_zeroconf( node_name = local_name[: -len(".local")] device_path = f"socket://{discovery_info.host}:{port}" - if current_entry := await self.async_set_unique_id(node_name): - self._abort_if_unique_id_configured( - updates={ - CONF_DEVICE: { - **current_entry.data.get(CONF_DEVICE, {}), - CONF_DEVICE_PATH: device_path, - }, - } - ) + await self._set_unique_id_or_update_path( + unique_id=node_name, + device_path=device_path, + ) self.context["title_placeholders"] = {CONF_NAME: node_name} self._title = device_path @@ -674,34 +699,31 @@ async def async_step_hardware( self, data: dict[str, Any] | None = None ) -> FlowResult: """Handle hardware flow.""" - if not data: - return self.async_abort(reason="invalid_hardware_data") - if data.get("radio_type") != "efr32": + try: + discovery_data = HARDWARE_DISCOVERY_SCHEMA(data) + except vol.Invalid: return self.async_abort(reason="invalid_hardware_data") - self._radio_type = RadioType.ezsp - - schema = { - vol.Required( - CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED - ): str - } - - radio_schema = self._radio_type.controller.SCHEMA_DEVICE.schema - assert not isinstance(radio_schema, vol.Schema) - - for param, value in radio_schema.items(): - if param in SUPPORTED_PORT_SETTINGS: - schema[param] = value + name = discovery_data["name"] + radio_type = self._parse_radio_type(discovery_data["radio_type"]) try: - device_settings = vol.Schema(schema)(data.get("port")) + device_settings = radio_type.controller.SCHEMA_DEVICE( + discovery_data["port"] + ) except vol.Invalid: return self.async_abort(reason="invalid_hardware_data") - self._title = data.get("name", data["port"]["path"]) + await self._set_unique_id_or_update_path( + unique_id=f"{name}_{radio_type.name}_{device_settings[CONF_DEVICE_PATH]}", + device_path=device_settings[CONF_DEVICE_PATH], + ) + + self._title = name + self._radio_type = radio_type self._device_path = device_settings[CONF_DEVICE_PATH] self._device_settings = device_settings + self.context["title_placeholders"] = {CONF_NAME: name} return await self.async_step_confirm() diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 725f9cc09178c9..dd6498fde4302c 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -987,6 +987,7 @@ async def test_hardware_already_setup(hass): ).add_to_hass(hass) data = { + "name": "Yellow", "radio_type": "efr32", "port": { "path": "/dev/ttyAMA1", From b9c47ed3c3c24da889b76e3d326bd26855246a32 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 7 Nov 2022 17:45:27 +0100 Subject: [PATCH 284/394] Align MQTT config entry setup strings with option flow (#81616) --- homeassistant/components/mqtt/strings.json | 48 +++++++++---------- .../components/mqtt/translations/en.json | 8 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 4799b45e631247..8d3a4ad3445087 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -19,10 +19,10 @@ "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", "advanced_options": "Advanced options", - "certificate": "Path to custom CA certificate file", + "certificate": "Upload custom CA certificate file", "client_id": "Client ID (leave empty to randomly generated one)", - "client_cert": "Path to a client certificate file", - "client_key": "Path to a private key file", + "client_cert": "Upload client certificate file", + "client_key": "Upload private key file", "keepalive": "The time between sending keep alive messages", "tls_insecure": "Ignore broker certificate validation", "protocol": "MQTT protocol", @@ -80,29 +80,29 @@ "step": { "broker": { "title": "Broker options", - "description": "Please enter the connection information of your MQTT broker.", + "description": "[%key:component::mqtt::config::step::broker::description%]", "data": { - "broker": "Broker", + "broker": "[%key:component::mqtt::config::step::broker::data::broker%]", "port": "[%key:common::config_flow::data::port%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", - "advanced_options": "Advanced options", - "certificate": "Upload custom CA certificate file", - "client_id": "Client ID (leave empty to randomly generated one)", - "client_cert": "Upload client certificate file", - "client_key": "Upload private key file", - "keepalive": "The time between sending keep alive messages", - "tls_insecure": "Ignore broker certificate validation", - "protocol": "MQTT protocol", - "set_ca_cert": "Broker certificate validation", - "set_client_cert": "Use a client certificate" + "advanced_options": "[%key:component::mqtt::config::step::broker::data::advanced_options%]", + "certificate": "[%key:component::mqtt::config::step::broker::data::certificate%]", + "client_id": "[%key:component::mqtt::config::step::broker::data::client_id%]", + "client_cert": "[%key:component::mqtt::config::step::broker::data::client_cert%]", + "client_key": "[%key:component::mqtt::config::step::broker::data::client_key%]", + "keepalive": "[%key:component::mqtt::config::step::broker::data::keepalive%]", + "tls_insecure": "[%key:component::mqtt::config::step::broker::data::tls_insecure%]", + "protocol": "[%key:component::mqtt::config::step::broker::data::protocol%]", + "set_ca_cert": "[%key:component::mqtt::config::step::broker::data::set_ca_cert%]", + "set_client_cert": "[%key:component::mqtt::config::step::broker::data::set_client_cert%]" } }, "options": { "title": "MQTT options", "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nDiscovery prefix - The prefix a configuration topic for automatic discovery must start with.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect.", "data": { - "discovery": "Enable discovery", + "discovery": "[%key:component::mqtt::config::step::hassio_confirm::data::discovery%]", "discovery_prefix": "Discovery prefix", "birth_enable": "Enable birth message", "birth_topic": "Birth message topic", @@ -118,15 +118,15 @@ } }, "error": { - "bad_birth": "Invalid birth topic", - "bad_will": "Invalid will topic", - "bad_discovery_prefix": "Invalid discovery prefix", - "bad_certificate": "The CA certificate is invalid", - "bad_client_cert": "Invalid client certiticate, ensure a PEM coded file is supplied", - "bad_client_key": "Invalid private key, ensure a PEM coded file is supplied without password", - "bad_client_cert_key": "Client certificate and private are no valid pair", + "bad_birth": "[%key:component::mqtt::config::error::bad_birth%]", + "bad_will": "[%key:component::mqtt::config::error::bad_will%]", + "bad_discovery_prefix": "[%key:component::mqtt::config::error::bad_discovery_prefix%]", + "bad_certificate": "[%key:component::mqtt::config::error::bad_certificate%]", + "bad_client_cert": "[%key:component::mqtt::config::error::bad_client_cert%]", + "bad_client_key": "[%key:component::mqtt::config::error::bad_client_key%]", + "bad_client_cert_key": "[%key:component::mqtt::config::error::bad_client_cert_key%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_inclusion": "The client certificate and private key must be configured together" + "invalid_inclusion": "[%key:component::mqtt::config::error::invalid_inclusion%]" } } } diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index f60f457abd387d..599fbf5be07def 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -13,17 +13,17 @@ "bad_discovery_prefix": "Invalid discovery prefix", "bad_will": "Invalid will topic", "cannot_connect": "Failed to connect", - "invalid_inclusion": "The client certificate and private key must be configurered together" + "invalid_inclusion": "The client certificate and private key must be configured together" }, "step": { "broker": { "data": { "advanced_options": "Advanced options", "broker": "Broker", - "certificate": "Path to custom CA certificate file", - "client_cert": "Path to a client certificate file", + "certificate": "Upload custom CA certificate file", + "client_cert": "Upload client certificate file", "client_id": "Client ID (leave empty to randomly generated one)", - "client_key": "Path to a private key file", + "client_key": "Upload private key file", "discovery": "Enable discovery", "keepalive": "The time between sending keep alive messages", "password": "Password", From 1b9c2dfb6869ade1b9b910413e54b46886f9562f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 7 Nov 2022 12:10:42 -0500 Subject: [PATCH 285/394] Bump pyunifiprotect to 4.4.1 (#81732) --- 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 62baf74b6a785f..7a412fede1d66a 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.4.0", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.4.1", "unifi-discovery==1.1.7"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index b60e9e1792e4a5..61919196caf34e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2087,7 +2087,7 @@ pytrafikverket==0.2.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.4.0 +pyunifiprotect==4.4.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f485e7453d56e5..fdb0a01bd23546 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1450,7 +1450,7 @@ pytrafikverket==0.2.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.4.0 +pyunifiprotect==4.4.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From e7a616b8ff1246331fba1d08711cfd955729df2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 7 Nov 2022 19:31:47 +0100 Subject: [PATCH 286/394] Use location info helper for IP in Cloudflare DNS (#81714) * Use location info helper for IP in Cloudflare DNS * simplify * Blow up * coverage --- .../components/cloudflare/__init__.py | 29 ++++++++-- tests/components/cloudflare/test_init.py | 58 ++++++++++++++++--- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index df5b08e9395a42..3a8f6b39ae71af 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging +from aiohttp import ClientSession from pycfdns import CloudflareUpdater from pycfdns.exceptions import ( CloudflareAuthenticationException, @@ -14,10 +15,16 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_TOKEN, CONF_ZONE from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.location import async_detect_location_info +from homeassistant.util.network import is_ipv4_address from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE_RECORDS @@ -28,8 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cloudflare from a config entry.""" + session = async_get_clientsession(hass) cfupdate = CloudflareUpdater( - async_get_clientsession(hass), + session, entry.data[CONF_API_TOKEN], entry.data[CONF_ZONE], entry.data[CONF_RECORDS], @@ -45,14 +53,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_records(now): """Set up recurring update.""" try: - await _async_update_cloudflare(cfupdate, zone_id) + await _async_update_cloudflare(session, cfupdate, zone_id) except CloudflareException as error: _LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error) async def update_records_service(call: ServiceCall) -> None: """Set up service for manual trigger.""" try: - await _async_update_cloudflare(cfupdate, zone_id) + await _async_update_cloudflare(session, cfupdate, zone_id) except CloudflareException as error: _LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error) @@ -76,11 +84,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def _async_update_cloudflare(cfupdate: CloudflareUpdater, zone_id: str): +async def _async_update_cloudflare( + session: ClientSession, + cfupdate: CloudflareUpdater, + zone_id: str, +) -> None: _LOGGER.debug("Starting update for zone %s", cfupdate.zone) records = await cfupdate.get_record_info(zone_id) _LOGGER.debug("Records: %s", records) - await cfupdate.update_records(zone_id, records) + location_info = await async_detect_location_info(session) + + if not location_info or not is_ipv4_address(location_info.ip): + raise HomeAssistantError("Could not get external IPv4 address") + + await cfupdate.update_records(zone_id, records, location_info.ip) _LOGGER.debug("Update for zone %s is complete", cfupdate.zone) diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index ab7dbdab78e9ad..6e7f450d7114da 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -1,11 +1,16 @@ """Test the Cloudflare integration.""" +from unittest.mock import patch + from pycfdns.exceptions import ( CloudflareAuthenticationException, CloudflareConnectionException, ) +import pytest from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.location import LocationInfo from . import ENTRY_CONFIG, init_integration @@ -70,12 +75,51 @@ async def test_integration_services(hass, cfupdate): entry = await init_integration(hass) assert entry.state is ConfigEntryState.LOADED - await hass.services.async_call( - DOMAIN, - SERVICE_UPDATE_RECORDS, - {}, - blocking=True, - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.cloudflare.async_detect_location_info", + return_value=LocationInfo( + "0.0.0.0", + "US", + "USD", + "CA", + "California", + "San Diego", + "92122", + "America/Los_Angeles", + 32.8594, + -117.2073, + True, + ), + ): + + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_RECORDS, + {}, + blocking=True, + ) + await hass.async_block_till_done() instance.update_records.assert_called_once() + + +async def test_integration_services_with_issue(hass, cfupdate): + """Test integration services with issue.""" + instance = cfupdate.return_value + + entry = await init_integration(hass) + assert entry.state is ConfigEntryState.LOADED + + with patch( + "homeassistant.components.cloudflare.async_detect_location_info", + return_value=None, + ), pytest.raises(HomeAssistantError, match="Could not get external IPv4 address"): + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_RECORDS, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + instance.update_records.assert_not_called() From bc146a09dba6df6eaec429039f6e1efcfbd1123f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 7 Nov 2022 13:39:29 -0500 Subject: [PATCH 287/394] Add integration_type for some integrations (#81499) --- homeassistant/components/deluge/manifest.json | 3 ++- homeassistant/components/discord/manifest.json | 3 ++- homeassistant/components/efergy/manifest.json | 3 ++- .../components/goalzero/manifest.json | 3 ++- .../components/google_sheets/manifest.json | 3 ++- homeassistant/components/lidarr/manifest.json | 3 ++- .../components/litterrobot/manifest.json | 3 ++- .../components/modem_callerid/manifest.json | 3 ++- .../components/nfandroidtv/manifest.json | 3 ++- homeassistant/components/radarr/manifest.json | 3 ++- homeassistant/components/skybell/manifest.json | 3 ++- .../components/steam_online/manifest.json | 3 ++- .../components/tautulli/manifest.json | 3 ++- homeassistant/generated/integrations.json | 18 +++++++++--------- 14 files changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index 920e560b70f1a6..89302d4cd48a15 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["deluge_client"] + "loggers": ["deluge_client"], + "integration_type": "service" } diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index b631c5fa7e726d..022cb5fd933d17 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -6,5 +6,6 @@ "requirements": ["nextcord==2.0.0a8"], "codeowners": ["@tkdrob"], "iot_class": "cloud_push", - "loggers": ["discord"] + "loggers": ["discord"], + "integration_type": "service" } diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index fc90591cae639b..b0cff7e203f33a 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -6,5 +6,6 @@ "requirements": ["pyefergy==22.1.1"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "loggers": ["iso4217", "pyefergy"] + "loggers": ["iso4217", "pyefergy"], + "integration_type": "hub" } diff --git a/homeassistant/components/goalzero/manifest.json b/homeassistant/components/goalzero/manifest.json index bb26567b8ccfdc..67e8bca2acc2d0 100644 --- a/homeassistant/components/goalzero/manifest.json +++ b/homeassistant/components/goalzero/manifest.json @@ -8,5 +8,6 @@ "codeowners": ["@tkdrob"], "quality_scale": "silver", "iot_class": "local_polling", - "loggers": ["goalzero"] + "loggers": ["goalzero"], + "integration_type": "device" } diff --git a/homeassistant/components/google_sheets/manifest.json b/homeassistant/components/google_sheets/manifest.json index c8d86210b42237..1c7790b1f25794 100644 --- a/homeassistant/components/google_sheets/manifest.json +++ b/homeassistant/components/google_sheets/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_sheets/", "requirements": ["gspread==5.5.0"], "codeowners": ["@tkdrob"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "integration_type": "service" } diff --git a/homeassistant/components/lidarr/manifest.json b/homeassistant/components/lidarr/manifest.json index eab24ef7e423f4..d9333470b009c8 100644 --- a/homeassistant/components/lidarr/manifest.json +++ b/homeassistant/components/lidarr/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["aiopyarr"] + "loggers": ["aiopyarr"], + "integration_type": "service" } diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index 6384df2f25a903..889c7edfd9c8ec 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -7,5 +7,6 @@ "codeowners": ["@natekspencer", "@tkdrob"], "dhcp": [{ "hostname": "litter-robot4" }], "iot_class": "cloud_push", - "loggers": ["pylitterbot"] + "loggers": ["pylitterbot"], + "integration_type": "hub" } diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index 024759791f42d9..58078a4ddb0d3d 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -8,5 +8,6 @@ "dependencies": ["usb"], "iot_class": "local_polling", "usb": [{ "vid": "0572", "pid": "1340" }], - "loggers": ["phone_modem"] + "loggers": ["phone_modem"], + "integration_type": "device" } diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index df285bea2284cf..fc05c2c12a1fff 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_push", - "loggers": ["notifications_android_tv"] + "loggers": ["notifications_android_tv"], + "integration_type": "service" } diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index 5117fd161d3d2f..1d1ce5b0289ca2 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@tkdrob"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["aiopyarr"] + "loggers": ["aiopyarr"], + "integration_type": "service" } diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index bfef4bc3422710..059ba76febc306 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -7,5 +7,6 @@ "dependencies": ["ffmpeg"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "loggers": ["aioskybell"] + "loggers": ["aioskybell"], + "integration_type": "hub" } diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index f8aba1aee0733a..4fb91943725405 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -6,5 +6,6 @@ "requirements": ["steamodd==4.21"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "loggers": ["steam"] + "loggers": ["steam"], + "integration_type": "service" } diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index bbdaa4c8ebbd2d..a77639e3a5819f 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -6,5 +6,6 @@ "config_flow": true, "codeowners": ["@ludeeus", "@tkdrob"], "iot_class": "local_polling", - "loggers": ["pytautulli"] + "loggers": ["pytautulli"], + "integration_type": "hub" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 69e7d22ba828d6..21e19326dc691d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -947,7 +947,7 @@ }, "deluge": { "name": "Deluge", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_polling" }, @@ -1046,7 +1046,7 @@ }, "discord": { "name": "Discord", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_push" }, @@ -1906,7 +1906,7 @@ }, "goalzero": { "name": "Goal Zero Yeti", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_polling" }, @@ -1951,7 +1951,7 @@ "name": "Google Pub/Sub" }, "google_sheets": { - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling", "name": "Google Sheets" @@ -2761,7 +2761,7 @@ }, "lidarr": { "name": "Lidarr", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_polling" }, @@ -3233,7 +3233,7 @@ }, "modem_callerid": { "name": "Phone Modem", - "integration_type": "hub", + "integration_type": "device", "config_flow": true, "iot_class": "local_polling" }, @@ -3480,7 +3480,7 @@ }, "nfandroidtv": { "name": "Notifications for Android TV / Fire TV", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_push" }, @@ -4211,7 +4211,7 @@ }, "radarr": { "name": "Radarr", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "local_polling" }, @@ -5038,7 +5038,7 @@ }, "steam_online": { "name": "Steam", - "integration_type": "hub", + "integration_type": "service", "config_flow": true, "iot_class": "cloud_polling" }, From 7f23ab9d86178a9fe00cfedd14cfee088270ce97 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 7 Nov 2022 15:46:41 -0500 Subject: [PATCH 288/394] Add measurement state class to eight_sleep sensors (#81589) * Add measurement state class to eight_sleep sensors * tweaks --- homeassistant/components/eight_sleep/sensor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index 7cce1293707372..58648123dcfdd7 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -7,7 +7,11 @@ from pyeight.eight import EightSleep import voluptuous as vol -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, UnitOfTemperature from homeassistant.core import HomeAssistant @@ -179,6 +183,9 @@ def __init__( elif self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"): self._attr_native_unit_of_measurement = "Score" + if self._sensor != "sleep_stage": + self._attr_state_class = SensorStateClass.MEASUREMENT + _LOGGER.debug( "User Sensor: %s, Side: %s, User: %s", self._sensor, @@ -272,6 +279,7 @@ class EightRoomSensor(EightSleepBaseEntity, SensorEntity): _attr_icon = "mdi:thermometer" _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_state_class = SensorStateClass.MEASUREMENT _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS def __init__( From 5c38321c4fd33d1c61cc5bf695ef389950cd77df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 15:50:45 -0600 Subject: [PATCH 289/394] Ignore unspecified addresses from zeroconf (#81620) --- homeassistant/components/zeroconf/__init__.py | 12 ++++++++++-- tests/components/zeroconf/test_init.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 62783e641d392b..82a9604a08c30e 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -552,12 +552,20 @@ def _first_non_link_local_address( """Return the first ipv6 or non-link local ipv4 address, preferring IPv4.""" for address in addresses: ip_addr = ip_address(address) - if not ip_addr.is_link_local and ip_addr.version == 4: + if ( + not ip_addr.is_link_local + and not ip_addr.is_unspecified + and ip_addr.version == 4 + ): return str(ip_addr) # If we didn't find a good IPv4 address, check for IPv6 addresses. for address in addresses: ip_addr = ip_address(address) - if not ip_addr.is_link_local and ip_addr.version == 6: + if ( + not ip_addr.is_link_local + and not ip_addr.is_unspecified + and ip_addr.version == 6 + ): return str(ip_addr) return None diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 039672b9955a19..a0684d6e10e746 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -819,6 +819,24 @@ async def test_info_from_service_with_link_local_address_first(hass): assert info.host == "192.168.66.12" +async def test_info_from_service_with_unspecified_address_first(hass): + """Test that the unspecified address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["0.0.0.0", "192.168.66.12"] + info = zeroconf.info_from_service(service_info) + assert info.host == "192.168.66.12" + + +async def test_info_from_service_with_unspecified_address_only(hass): + """Test that the unspecified address is ignored.""" + service_type = "_test._tcp.local." + service_info = get_service_info_mock(service_type, f"test.{service_type}") + service_info.addresses = ["0.0.0.0"] + info = zeroconf.info_from_service(service_info) + assert info is None + + async def test_info_from_service_with_link_local_address_second(hass): """Test that the link local address is ignored.""" service_type = "_test._tcp.local." From 55cad465b209aee179ad1edfb3ee7de0ffc84342 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 8 Nov 2022 00:07:02 +0100 Subject: [PATCH 290/394] Add support for AEH with adjustable temperature in Overkiz integration (#72790) * Import and clean from ha-tahoma * Fix wrong search and replace * Clean code * Use elif * Sort import * Fix imports * Update homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py Co-authored-by: Mick Vleeshouwer * Fix import * Remove cast * Remove useless constructor * Use constant * Remove now useless conditions Co-authored-by: Mick Vleeshouwer --- .../overkiz/climate_entities/__init__.py | 4 + ...er_with_adjustable_temperature_setpoint.py | 134 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + 3 files changed, 139 insertions(+) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 359f7629a7b11d..5d676e497472d2 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -2,6 +2,9 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater +from .atlantic_electrical_heater_with_adjustable_temperature_setpoint import ( + AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint, +) from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer from .atlantic_heat_recovery_ventilation import AtlanticHeatRecoveryVentilation from .atlantic_pass_apc_heating_and_cooling_zone import ( @@ -12,6 +15,7 @@ WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, + UIWidget.ATLANTIC_ELECTRICAL_HEATER_WITH_ADJUSTABLE_TEMPERATURE_SETPOINT: AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint, UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: AtlanticHeatRecoveryVentilation, UIWidget.ATLANTIC_PASS_APC_HEATING_AND_COOLING_ZONE: AtlanticPassAPCHeatingAndCoolingZone, diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py new file mode 100644 index 00000000000000..80c2f418331d62 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_heater_with_adjustable_temperature_setpoint.py @@ -0,0 +1,134 @@ +"""Support for Atlantic Electrical Heater (With Adjustable Temperature Setpoint).""" +from __future__ import annotations + +from typing import Any + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ( + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, + PRESET_NONE, + ClimateEntity, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +PRESET_AUTO = "auto" +PRESET_COMFORT1 = "comfort-1" +PRESET_COMFORT2 = "comfort-2" +PRESET_FROST_PROTECTION = "frost_protection" +PRESET_PROG = "prog" + + +# Map Overkiz presets to Home Assistant presets +OVERKIZ_TO_PRESET_MODE: dict[str, str] = { + OverkizCommandParam.OFF: PRESET_NONE, + OverkizCommandParam.FROSTPROTECTION: PRESET_FROST_PROTECTION, + OverkizCommandParam.ECO: PRESET_ECO, + OverkizCommandParam.COMFORT: PRESET_COMFORT, + OverkizCommandParam.COMFORT_1: PRESET_COMFORT1, + OverkizCommandParam.COMFORT_2: PRESET_COMFORT2, + OverkizCommandParam.AUTO: PRESET_AUTO, + OverkizCommandParam.BOOST: PRESET_BOOST, + OverkizCommandParam.INTERNAL: PRESET_PROG, +} + +PRESET_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODE.items()} + +# Map Overkiz HVAC modes to Home Assistant HVAC modes +OVERKIZ_TO_HVAC_MODE: dict[str, str] = { + OverkizCommandParam.ON: HVACMode.HEAT, + OverkizCommandParam.OFF: HVACMode.OFF, + OverkizCommandParam.AUTO: HVACMode.AUTO, + OverkizCommandParam.BASIC: HVACMode.HEAT, + OverkizCommandParam.STANDBY: HVACMode.OFF, + OverkizCommandParam.INTERNAL: HVACMode.AUTO, +} + +HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()} + +TEMPERATURE_SENSOR_DEVICE_INDEX = 2 + + +class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint( + OverkizEntity, ClimateEntity +): + """Representation of Atlantic Electrical Heater (With Adjustable Temperature Setpoint).""" + + _attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ] + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ) + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self.temperature_device = self.executor.linked_device( + TEMPERATURE_SENSOR_DEVICE_INDEX + ) + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + states = self.device.states + if (state := states[OverkizState.CORE_OPERATING_MODE]) and state.value_as_str: + return OVERKIZ_TO_HVAC_MODE[state.value_as_str] + if (state := states[OverkizState.CORE_ON_OFF]) and state.value_as_str: + return OVERKIZ_TO_HVAC_MODE[state.value_as_str] + return HVACMode.OFF + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + await self.executor.async_execute_command( + OverkizCommand.SET_OPERATING_MODE, HVAC_MODE_TO_OVERKIZ[hvac_mode] + ) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., home, away, temp.""" + if ( + state := self.device.states[OverkizState.IO_TARGET_HEATING_LEVEL] + ) and state.value_as_str: + return OVERKIZ_TO_PRESET_MODE[state.value_as_str] + return None + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode in [PRESET_AUTO, PRESET_PROG]: + command = OverkizCommand.SET_OPERATING_MODE + else: + command = OverkizCommand.SET_HEATING_LEVEL + await self.executor.async_execute_command( + command, PRESET_MODE_TO_OVERKIZ[preset_mode] + ) + + @property + def target_temperature(self) -> float | None: + """Return the temperature.""" + if state := self.device.states[OverkizState.CORE_TARGET_TEMPERATURE]: + return state.value_as_float + return None + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return temperature.value_as_float + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_TEMPERATURE, temperature + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 4645b058182aed..f207fc305e0aee 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -63,6 +63,7 @@ UIClass.WINDOW: Platform.COVER, UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_ELECTRICAL_HEATER_WITH_ADJUSTABLE_TEMPERATURE_SETPOINT: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_HEAT_RECOVERY_VENTILATION: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_DHW: Platform.WATER_HEATER, # widgetName, uiClass is WaterHeatingSystem (not supported) From 785cf0e29caa7aea1e5b9cbc0e02e1cd9515cac1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 8 Nov 2022 00:26:39 +0000 Subject: [PATCH 291/394] [ci skip] Translation update --- .../components/airq/translations/hu.json | 22 +++++++++++++++++++ .../components/airq/translations/ru.json | 2 +- .../components/androidtv/translations/ru.json | 2 +- .../components/asuswrt/translations/ru.json | 2 +- .../components/braviatv/translations/de.json | 3 +++ .../components/braviatv/translations/es.json | 3 +++ .../components/braviatv/translations/et.json | 3 +++ .../components/braviatv/translations/id.json | 3 +++ .../components/braviatv/translations/no.json | 3 +++ .../braviatv/translations/pt-BR.json | 3 +++ .../components/braviatv/translations/ru.json | 5 ++++- .../braviatv/translations/zh-Hant.json | 3 +++ .../components/broadlink/translations/ru.json | 4 ++-- .../components/brother/translations/ru.json | 2 +- .../components/dnsip/translations/ru.json | 4 ++-- .../components/dunehd/translations/ru.json | 2 +- .../components/ezviz/translations/ru.json | 2 +- .../forecast_solar/translations/hu.json | 2 +- .../components/fronius/translations/ru.json | 4 ++-- .../components/goalzero/translations/ru.json | 4 ++-- .../components/livisi/translations/de.json | 18 +++++++++++++++ .../components/livisi/translations/es.json | 18 +++++++++++++++ .../components/livisi/translations/hu.json | 18 +++++++++++++++ .../components/livisi/translations/pt-BR.json | 18 +++++++++++++++ .../components/livisi/translations/ru.json | 18 +++++++++++++++ .../components/mqtt/translations/de.json | 10 ++++----- .../components/mqtt/translations/en.json | 4 ++-- .../components/mqtt/translations/es.json | 6 ++--- .../components/mqtt/translations/hu.json | 12 +++++----- .../nibe_heatpump/translations/ru.json | 2 +- .../p1_monitor/translations/ru.json | 2 +- .../pure_energie/translations/ru.json | 2 +- .../rainmachine/translations/ru.json | 2 +- .../components/senseme/translations/ru.json | 2 +- .../components/tolo/translations/ru.json | 2 +- .../unifiprotect/translations/de.json | 6 +++++ .../unifiprotect/translations/en.json | 4 ++++ .../unifiprotect/translations/es.json | 6 +++++ .../unifiprotect/translations/et.json | 4 ++-- .../unifiprotect/translations/hu.json | 6 +++++ .../unifiprotect/translations/id.json | 6 +++++ .../unifiprotect/translations/no.json | 6 +++++ .../unifiprotect/translations/pt-BR.json | 6 +++++ .../unifiprotect/translations/zh-Hant.json | 6 +++++ .../components/vallox/translations/ru.json | 4 ++-- .../xiaomi_aqara/translations/ru.json | 2 +- 46 files changed, 225 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/airq/translations/hu.json create mode 100644 homeassistant/components/livisi/translations/de.json create mode 100644 homeassistant/components/livisi/translations/es.json create mode 100644 homeassistant/components/livisi/translations/hu.json create mode 100644 homeassistant/components/livisi/translations/pt-BR.json create mode 100644 homeassistant/components/livisi/translations/ru.json diff --git a/homeassistant/components/airq/translations/hu.json b/homeassistant/components/airq/translations/hu.json new file mode 100644 index 00000000000000..adfcb73e289867 --- /dev/null +++ b/homeassistant/components/airq/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_input": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm", + "password": "Jelsz\u00f3" + }, + "description": "Adja meg az eszk\u00f6z IP-c\u00edm\u00e9t vagy mDNS c\u00edm\u00e9t \u00e9s jelszav\u00e1t.", + "title": "Az eszk\u00f6z azonos\u00edt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/ru.json b/homeassistant/components/airq/translations/ru.json index 416787acc7f9a8..b2456f01a33c7b 100644 --- a/homeassistant/components/airq/translations/ru.json +++ b/homeassistant/components/airq/translations/ru.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_input": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_input": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "step": { "user": { diff --git a/homeassistant/components/androidtv/translations/ru.json b/homeassistant/components/androidtv/translations/ru.json index 61eb431fbf479a..4bb20ad0374af0 100644 --- a/homeassistant/components/androidtv/translations/ru.json +++ b/homeassistant/components/androidtv/translations/ru.json @@ -7,7 +7,7 @@ "error": { "adbkey_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 ADB \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "key_and_server": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043b\u044e\u0447 ADB \u0438\u043b\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 ADB.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index 0253cd20d1b11f..770cff7eb390f1 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "pwd_and_ssh": "\u041d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", "pwd_or_ssh": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0438\u043b\u0438 \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH.", "ssh_not_file": "\u0424\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 SSH \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index f62d496f2d3b83..492e647f3029b4 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Beim Aktualisieren der Quellenliste ist ein Fehler aufgetreten. \n\nStelle sicher, dass dein Fernseher eingeschaltet ist, bevor du versuchst, ihn einzurichten." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index fbcd69a0bec81f..c467c5dafca675 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Ocurri\u00f3 un error al actualizar la lista de fuentes. \n\nAseg\u00farate de que tu TV est\u00e9 encendida antes de intentar configurarla." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index 4e3ca6333d4e1e..c650b6abd9f6d3 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Allikate loendi v\u00e4rskendamisel ilmnes viga. \n\n Enne teleri seadistamist veendu, et see oleks sisse l\u00fclitatud." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index 61b5b17ff1e316..6c357e6e2bbd65 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Terjadi kesalahan saat memperbarui daftar sumber.\n\nPastikan TV Anda sudah dihidupkan sebelum menyiapkan." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index dec2157d6a35af..a568e364c12a63 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Det oppsto en feil under oppdatering av kildelisten. \n\n S\u00f8rg for at TV-en er sl\u00e5tt p\u00e5 f\u00f8r du pr\u00f8ver \u00e5 sette den opp." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index 7c5af6e269470f..e048568e351827 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Ocorreu um erro ao atualizar a lista de fontes. \n\n Certifique-se de que sua TV esteja ligada antes de tentar configur\u00e1-la." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 8416d9e5ede067..299ad538bc693d 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, "step": { @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. \n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index e30142c947b57d..c66ba705db1aed 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "\u66f4\u65b0\u4f86\u6e90\u5217\u8868\u6642\u767c\u751f\u932f\u8aa4\u3002\n\n\u8acb\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u65bc\u8a2d\u5b9a\u524d\u958b\u555f\u3002" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/broadlink/translations/ru.json b/homeassistant/components/broadlink/translations/ru.json index 65ee1f4db1db86..85fd5db28e4478 100644 --- a/homeassistant/components/broadlink/translations/ru.json +++ b/homeassistant/components/broadlink/translations/ru.json @@ -4,13 +4,13 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "{name} ({model}, {host})", diff --git a/homeassistant/components/brother/translations/ru.json b/homeassistant/components/brother/translations/ru.json index a9f6158ccf86a3..1469e9962ed2a6 100644 --- a/homeassistant/components/brother/translations/ru.json +++ b/homeassistant/components/brother/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", - "wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{model} {serial_number}", "step": { diff --git a/homeassistant/components/dnsip/translations/ru.json b/homeassistant/components/dnsip/translations/ru.json index a4421b566f60f7..0882153776a9b8 100644 --- a/homeassistant/components/dnsip/translations/ru.json +++ b/homeassistant/components/dnsip/translations/ru.json @@ -1,12 +1,12 @@ { "config": { "error": { - "invalid_hostname": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f." + "invalid_hostname": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430." }, "step": { "user": { "data": { - "hostname": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f DNS-\u0437\u0430\u043f\u0440\u043e\u0441", + "hostname": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0431\u0443\u0434\u0435\u0442 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c\u0441\u044f DNS-\u0437\u0430\u043f\u0440\u043e\u0441", "resolver": "\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 IPV4", "resolver_ipv6": "\u0420\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 IPV6" } diff --git a/homeassistant/components/dunehd/translations/ru.json b/homeassistant/components/dunehd/translations/ru.json index 8c32af72af7659..f0d5e989dd2fcd 100644 --- a/homeassistant/components/dunehd/translations/ru.json +++ b/homeassistant/components/dunehd/translations/ru.json @@ -6,7 +6,7 @@ "error": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "step": { "user": { diff --git a/homeassistant/components/ezviz/translations/ru.json b/homeassistant/components/ezviz/translations/ru.json index 13bdf601817afe..77c3332d0bcb4e 100644 --- a/homeassistant/components/ezviz/translations/ru.json +++ b/homeassistant/components/ezviz/translations/ru.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{serial}", "step": { diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 3aac09afe1bed1..7ff078d130197c 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -25,7 +25,7 @@ "inverter_size": "Inverter m\u00e9rete (Watt)", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" }, - "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 kit\u00f6lt\u00e9se nem egy\u00e9rtelm\u0171." + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Forecast.Solar eredm\u00e9ny\u00e9nek finomhangol\u00e1s\u00e1t. Ha egy mez\u0151 nem egy\u00e9rtelm\u0171, k\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t." } } } diff --git a/homeassistant/components/fronius/translations/ru.json b/homeassistant/components/fronius/translations/ru.json index 473834c87970b0..12d0e0120839b9 100644 --- a/homeassistant/components/fronius/translations/ru.json +++ b/homeassistant/components/fronius/translations/ru.json @@ -2,7 +2,7 @@ "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.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -17,7 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Fronius.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Fronius.", "title": "Fronius SolarNet" } } diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json index 7bd8c3df3114a3..ed028936aa3592 100644 --- a/homeassistant/components/goalzero/translations/ru.json +++ b/homeassistant/components/goalzero/translations/ru.json @@ -2,12 +2,12 @@ "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.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/livisi/translations/de.json b/homeassistant/components/livisi/translations/de.json new file mode 100644 index 00000000000000..d85e61a13db6bc --- /dev/null +++ b/homeassistant/components/livisi/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "wrong_ip_address": "Die IP-Adresse ist falsch oder der SHC ist lokal nicht erreichbar.", + "wrong_password": "Das Passwort ist falsch." + }, + "step": { + "user": { + "data": { + "host": "IP-Adresse", + "password": "Passwort" + }, + "description": "Gib die IP-Adresse und das (lokale) Passwort des SHC ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/es.json b/homeassistant/components/livisi/translations/es.json new file mode 100644 index 00000000000000..87a51cc79facb6 --- /dev/null +++ b/homeassistant/components/livisi/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "wrong_ip_address": "La direcci\u00f3n IP es incorrecta o no se puede acceder localmente al SHC.", + "wrong_password": "La contrase\u00f1a es incorrecta." + }, + "step": { + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a" + }, + "description": "Introduce la direcci\u00f3n IP y la contrase\u00f1a (local) del SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/hu.json b/homeassistant/components/livisi/translations/hu.json new file mode 100644 index 00000000000000..d203bc3c7c4901 --- /dev/null +++ b/homeassistant/components/livisi/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "wrong_ip_address": "Az IP-c\u00edm helytelen, vagy az SHC nem \u00e9rhet\u0151 el a helyi h\u00e1l\u00f3zatban.", + "wrong_password": "A jelsz\u00f3 helytelen." + }, + "step": { + "user": { + "data": { + "host": "IP c\u00edm", + "password": "Jelsz\u00f3" + }, + "description": "Adja meg az SHC helyi IP-c\u00edm\u00e9t \u00e9s jelszav\u00e1t." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/pt-BR.json b/homeassistant/components/livisi/translations/pt-BR.json new file mode 100644 index 00000000000000..ff56d2c5aca8df --- /dev/null +++ b/homeassistant/components/livisi/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Falhou ao conectar", + "wrong_ip_address": "O endere\u00e7o IP est\u00e1 incorreto ou o SHC n\u00e3o pode ser alcan\u00e7ado localmente.", + "wrong_password": "A senha est\u00e1 incorreta." + }, + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP", + "password": "Senha" + }, + "description": "Digite o endere\u00e7o IP e a senha (local) do SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/ru.json b/homeassistant/components/livisi/translations/ru.json new file mode 100644 index 00000000000000..9fcbe468154249 --- /dev/null +++ b/homeassistant/components/livisi/translations/ru.json @@ -0,0 +1,18 @@ +{ + "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.", + "wrong_ip_address": "IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0432\u0435\u0440\u0435\u043d \u0438\u043b\u0438 SHC \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e.", + "wrong_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0438 (\u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439) \u043f\u0430\u0440\u043e\u043b\u044c SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index b193f18b39e056..425425a24a1b4f 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Erweiterte Optionen", "broker": "Server", - "certificate": "Pfad zur benutzerdefinierten CA-Zertifikatsdatei", - "client_cert": "Pfad zur Client-Zertifikatsdatei", + "certificate": "Hochladen einer benutzerdefinierten CA-Zertifikatsdatei", + "client_cert": "Client-Zertifikatsdatei hochladen", "client_id": "Client-ID (leer lassen, um eine zuf\u00e4llig generierte zu erhalten)", - "client_key": "Pfad zur privaten Schl\u00fcsseldatei", + "client_key": "Private Schl\u00fcsseldatei hochladen", "discovery": "Suche aktivieren", "keepalive": "Die Zeit zwischen dem Senden von Keep-Alive-Nachrichten", "password": "Passwort", @@ -93,7 +93,7 @@ "broker": { "data": { "advanced_options": "Erweiterte Optionen", - "broker": "Broker", + "broker": "Server", "certificate": "Hochladen einer benutzerdefinierten CA-Zertifikatsdatei", "client_cert": "Client-Zertifikatsdatei hochladen", "client_id": "Client-ID (leer lassen, um eine zuf\u00e4llig generierte zu erhalten)", @@ -117,7 +117,7 @@ "birth_qos": "Birth Nachricht QoS", "birth_retain": "Birth Nachricht zwischenspeichern", "birth_topic": "Thema der Birth Nachricht", - "discovery": "Erkennung aktivieren", + "discovery": "Suche aktivieren", "discovery_prefix": "Discovery-Pr\u00e4fix", "will_enable": "Letzten Willen aktivieren", "will_payload": "Nutzdaten der Letzter-Wille Nachricht", diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 599fbf5be07def..74ecf3ccb11019 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -13,7 +13,7 @@ "bad_discovery_prefix": "Invalid discovery prefix", "bad_will": "Invalid will topic", "cannot_connect": "Failed to connect", - "invalid_inclusion": "The client certificate and private key must be configured together" + "invalid_inclusion": "The client certificate and private key must be configurered together" }, "step": { "broker": { @@ -87,7 +87,7 @@ "bad_discovery_prefix": "Invalid discovery prefix", "bad_will": "Invalid will topic", "cannot_connect": "Failed to connect", - "invalid_inclusion": "The client certificate and private key must be configured together" + "invalid_inclusion": "The client certificate and private key must be configurered together" }, "step": { "broker": { diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 8c05afe997ab60..8ad74693dde1ed 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Opciones avanzadas", "broker": "Br\u00f3ker", - "certificate": "Ruta al archivo de certificado de la CA personalizado", - "client_cert": "Ruta a un archivo de certificado de cliente", + "certificate": "Subir archivo de certificado de la CA personalizado", + "client_cert": "Subir archivo de certificado de cliente", "client_id": "ID de cliente (dejar vac\u00edo para generar uno aleatoriamente)", - "client_key": "Ruta a un archivo de clave privada", + "client_key": "Subir archivo de clave privada", "discovery": "Habilitar descubrimiento", "keepalive": "El tiempo entre el env\u00edo de mensajes keep alive", "password": "Contrase\u00f1a", diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index c643f0ef40458a..cbc557d40216a0 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Speci\u00e1lis be\u00e1ll\u00edt\u00e1sok", "broker": "Br\u00f3ker", - "certificate": "Az egy\u00e9ni CA-tan\u00fas\u00edtv\u00e1nyf\u00e1jl el\u00e9r\u00e9si \u00fatja", - "client_cert": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny f\u00e1jl el\u00e9r\u00e9si \u00fatja", + "certificate": "Egy\u00e9ni CA-tan\u00fas\u00edtv\u00e1nyf\u00e1jl felt\u00f6lt\u00e9se", + "client_cert": "\u00dcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny f\u00e1jl felt\u00f6lt\u00e9se", "client_id": "\u00dcgyf\u00e9l azonos\u00edt\u00f3 (hagyja \u00fcresen a v\u00e9letlenszer\u0171en gener\u00e1lt azonos\u00edt\u00f3hoz)", - "client_key": "A priv\u00e1t kulcsf\u00e1jl el\u00e9r\u00e9si \u00fatvonala", + "client_key": "Priv\u00e1t kulcsf\u00e1jl felt\u00f6lt\u00e9se", "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se", "keepalive": "A keep alive \u00fczenetek k\u00fcld\u00e9se k\u00f6z\u00f6tti id\u0151", "password": "Jelsz\u00f3", @@ -79,15 +79,15 @@ }, "options": { "error": { - "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik.", + "bad_birth": "\u00c9rv\u00e9nytelen 'birth' topik", "bad_certificate": "A CA-tan\u00fas\u00edtv\u00e1ny \u00e9rv\u00e9nytelen", "bad_client_cert": "\u00c9rv\u00e9nytelen \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny. Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dolt f\u00e1jl van megadva", "bad_client_cert_key": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1ny \u00e9s a priv\u00e1t tan\u00fas\u00edtv\u00e1ny nem \u00e9rv\u00e9nyes p\u00e1r", "bad_client_key": "\u00c9rv\u00e9nytelen priv\u00e1t kulcs, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy PEM k\u00f3dol\u00e1s\u00fa f\u00e1jlt k\u00fcld\u00f6tt jelsz\u00f3 n\u00e9lk\u00fcl", "bad_discovery_prefix": "\u00c9rv\u00e9nytelen felfedez\u00e9si el\u0151tag", - "bad_will": "\u00c9rv\u00e9nytelen 'will' topik.", + "bad_will": "\u00c9rv\u00e9nytelen 'will' topik", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_inclusion": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1nyt \u00e9s a priv\u00e1t kulcsot egy\u00fctt kell konfigur\u00e1lni" + "invalid_inclusion": "Az \u00fcgyf\u00e9ltan\u00fas\u00edtv\u00e1nyt \u00e9s a mag\u00e1nkulcsot egy\u00fctt kell konfigur\u00e1lni" }, "step": { "broker": { diff --git a/homeassistant/components/nibe_heatpump/translations/ru.json b/homeassistant/components/nibe_heatpump/translations/ru.json index f280ae2ba57247..59f228b37460b9 100644 --- a/homeassistant/components/nibe_heatpump/translations/ru.json +++ b/homeassistant/components/nibe_heatpump/translations/ru.json @@ -4,7 +4,7 @@ "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": { - "address": "\u0423\u043a\u0430\u0437\u0430\u043d \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441. \u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f.", + "address": "\u0423\u043a\u0430\u0437\u0430\u043d \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441. \u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430.", "address_in_use": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u044d\u0442\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", "model": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 modbus40.", "read": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`.", diff --git a/homeassistant/components/p1_monitor/translations/ru.json b/homeassistant/components/p1_monitor/translations/ru.json index 75a179abdc3f5f..10524a44fa56df 100644 --- a/homeassistant/components/p1_monitor/translations/ru.json +++ b/homeassistant/components/p1_monitor/translations/ru.json @@ -10,7 +10,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "data_description": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 P1 Monitor." + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0439 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 P1 Monitor." }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 P1 Monitor." } diff --git a/homeassistant/components/pure_energie/translations/ru.json b/homeassistant/components/pure_energie/translations/ru.json index 2aa39757104760..e2f7bf36745753 100644 --- a/homeassistant/components/pure_energie/translations/ru.json +++ b/homeassistant/components/pure_energie/translations/ru.json @@ -14,7 +14,7 @@ "host": "\u0425\u043e\u0441\u0442" }, "data_description": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0433\u043e Pure Energie Meter." + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0412\u0430\u0448\u0435\u0433\u043e Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/rainmachine/translations/ru.json b/homeassistant/components/rainmachine/translations/ru.json index 03fbe1fb0743f4..c2c904fd0a8140 100644 --- a/homeassistant/components/rainmachine/translations/ru.json +++ b/homeassistant/components/rainmachine/translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "ip_address": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/senseme/translations/ru.json b/homeassistant/components/senseme/translations/ru.json index 8debd33481f537..905f670a1f9b09 100644 --- a/homeassistant/components/senseme/translations/ru.json +++ b/homeassistant/components/senseme/translations/ru.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{name} - {model} ({host})", "step": { diff --git a/homeassistant/components/tolo/translations/ru.json b/homeassistant/components/tolo/translations/ru.json index 0243a40cf7e781..e9ff9c6552f7db 100644 --- a/homeassistant/components/tolo/translations/ru.json +++ b/homeassistant/components/tolo/translations/ru.json @@ -15,7 +15,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." } } } diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json index f44ad32a2b3922..e283364599dbc3 100644 --- a/homeassistant/components/unifiprotect/translations/de.json +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Du verwendest v{version} von UniFi Protect, einer Early-Access-Version. Early-Access-Versionen werden von Home Assistant nicht unterst\u00fctzt und k\u00f6nnen dazu f\u00fchren, dass deine UniFi Protect-Integration nicht oder nicht wie erwartet funktioniert.", + "title": "UniFi Protect v{version} ist eine Early-Access-Version" + } + }, "options": { "error": { "invalid_mac_list": "Muss eine durch Kommas getrennte Liste von MAC-Adressen sein" diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 033626298796b4..742a1b3a851a61 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -48,11 +48,15 @@ } }, "options": { + "error": { + "invalid_mac_list": "Must be a list of MAC addresses seperated by commas" + }, "step": { "init": { "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", + "ignored_devices": "Comma separated list of MAC addresses of devices to ignore", "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index e278fb6ecf067b..d3a3f9a232935a 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Est\u00e1s utilizando v{version} de UniFi Protect, que es una versi\u00f3n Early Access. Las versiones Early Access no son compatibles con Home Assistant y pueden causar que tu integraci\u00f3n con UniFi Protect no funcione por completo o como se esperaba.", + "title": "UniFi Protect v{version} es una versi\u00f3n Early Access" + } + }, "options": { "error": { "invalid_mac_list": "Debe ser una lista de direcciones MAC separadas por comas" diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index 9ddf72f925d352..54225737c779b0 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -43,8 +43,8 @@ }, "issues": { "ea_warning": { - "description": "Kasutad UniFi Protecti {version}. Home Assistant ei toeta varajase juurdep\u00e4\u00e4su versioone ja see v\u00f5ib p\u00f5hjustada UniFi Protecti sidumise katkemist v\u00f5i ei t\u00f6\u00f6ta see ootusp\u00e4raselt.", - "title": "{version} on UniFi Protecti varajase juurdep\u00e4\u00e4su versioon" + "description": "Kasutad UniFi Protecti v{version}. Home Assistant ei toeta varajase juurdep\u00e4\u00e4su versioone ja see v\u00f5ib p\u00f5hjustada UniFi Protecti sidumise katkemist v\u00f5i ei t\u00f6\u00f6ta see ootusp\u00e4raselt.", + "title": "v{version} on UniFi Protecti varajase juurdep\u00e4\u00e4su versioon" } }, "options": { diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index dac543f97c23fb..477374a074f29a 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "\u00d6n az UniFi Protect {version} verzi\u00f3j\u00e1t haszn\u00e1lja. A korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3kat Home Assistant nem t\u00e1mogatja, \u00e9s az UniFi Protect integr\u00e1ci\u00f3ja le\u00e1llhat, vagy nem az elv\u00e1rt m\u00f3don m\u0171k\u00f6dhet.", + "title": "{version} UniFi Protect korai hozz\u00e1f\u00e9r\u00e9si verzi\u00f3" + } + }, "options": { "error": { "invalid_mac_list": "Egy list\u00e1ra van sz\u00fcks\u00e9g, melyben a MAC-c\u00edmek vessz\u0151vel vannak elv\u00e1lasztva" diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index 772c339f3df3f0..cae7dfd3253b37 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Anda menggunakan UniFi Protect v{version} yang merupakan versi Early Access. Versi Early Access tidak didukung oleh Home Assistant dan dapat menyebabkan integrasi UniFi Protect rusak atau tidak berfungsi seperti yang diharapkan.", + "title": "UniFi Protect v{version} adalah versi Early Access" + } + }, "options": { "error": { "invalid_mac_list": "Harus berupa daftar alamat MAC yang dipisahkan dengan koma" diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index 7eac0b2dca148c..1ac28d4221a571 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Du bruker v {version} av UniFi Protect som er en tidlig tilgangsversjon. Early Access-versjoner st\u00f8ttes ikke av Home Assistant og kan f\u00f8re til at UniFi Protect-integrasjonen din g\u00e5r i stykker eller ikke fungerer som forventet.", + "title": "UniFi Protect v {version} er en versjon med tidlig tilgang" + } + }, "options": { "error": { "invalid_mac_list": "M\u00e5 v\u00e6re en liste over MAC-adresser atskilt med komma" diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 3bc780b57b7b6b..441ef2e7ab6664 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "Voc\u00ea est\u00e1 usando v {version} do UniFi Protect, que \u00e9 uma vers\u00e3o de acesso antecipado. As vers\u00f5es de acesso antecipado n\u00e3o s\u00e3o suportadas pelo Home Assistant e podem fazer com que a integra\u00e7\u00e3o do UniFi Protect seja interrompida ou n\u00e3o funcione conforme o esperado.", + "title": "UniFi Protect v {version} \u00e9 uma vers\u00e3o de acesso antecipado" + } + }, "options": { "error": { "invalid_mac_list": "Deve ser uma lista de endere\u00e7os MAC separados por v\u00edrgulas" diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index 0688a40d0c8ec3..ce1498044f3d24 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "\u6b63\u5728\u4f7f\u7528\u7684 UniFi Protect {version} \u7248\u3001\u70ba Home Assistant \u4e0d\u652f\u63f4\u7684\u6436\u5148\u9ad4\u9a57\u7248\u672c\uff0c\u53ef\u80fd\u6703\u5c0e\u81f4 UniFi Protect \u6574\u5408\u51fa\u73fe\u554f\u984c\u3001\u6216\u7121\u6cd5\u6b63\u5e38\u5de5\u4f5c\u3002", + "title": "UniFi Protect {version} \u7248\u70ba\u6436\u5148\u9ad4\u9a57\u7248" + } + }, "options": { "error": { "invalid_mac_list": "\u5fc5\u9808\u70ba\u4ee5\u9017\u865f\uff08\uff1a\uff09\u5206\u9694\u958b\u7684 MAC \u5730\u5740\u5217\u8868" diff --git a/homeassistant/components/vallox/translations/ru.json b/homeassistant/components/vallox/translations/ru.json index a1165b287d16e2..990357a015e771 100644 --- a/homeassistant/components/vallox/translations/ru.json +++ b/homeassistant/components/vallox/translations/ru.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 3483bf2d3f8cfb..6bef9c3d4b5150 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -7,7 +7,7 @@ }, "error": { "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Xiaomi Aqara, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 HomeAssistant \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430.", - "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.. \u0421\u043f\u043e\u0441\u043e\u0431\u044b \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0437\u0434\u0435\u0441\u044c: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.. \u0421\u043f\u043e\u0441\u043e\u0431\u044b \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0437\u0434\u0435\u0441\u044c: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.", "invalid_interface": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0430.", "invalid_mac": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 MAC-\u0430\u0434\u0440\u0435\u0441." From c2c26e2608b3f676424dd08f730ce4c49c8b319e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Nov 2022 21:19:57 -0600 Subject: [PATCH 292/394] Fix check for duplicate config entry reauth when context is passed or augmented (#81753) fixes https://github.com/home-assistant/core/issues/77578 --- homeassistant/config_entries.py | 29 +++++++++++++++-------------- tests/test_config_entries.py | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8e618deb3d21e9..b7ee9a4d6549db 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -660,24 +660,25 @@ def async_start_reauth( data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" - flow_context = { - "source": SOURCE_REAUTH, - "entry_id": self.entry_id, - "title_placeholders": {"name": self.title}, - "unique_id": self.unique_id, - } - - if context: - flow_context.update(context) - - for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): - if flow["context"] == flow_context: - return + if any( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"].get("source") == SOURCE_REAUTH + and flow["context"].get("entry_id") == self.entry_id + ): + # Reauth flow already in progress for this entry + return hass.async_create_task( hass.config_entries.flow.async_init( self.domain, - context=flow_context, + context={ + "source": SOURCE_REAUTH, + "entry_id": self.entry_id, + "title_placeholders": {"name": self.title}, + "unique_id": self.unique_id, + } + | (context or {}), data=self.data | (data or {}), ) ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 83343146d47030..99e26be6d750a6 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3287,6 +3287,7 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): async def test_reauth(hass): """Test the async_reauth_helper.""" entry = MockConfigEntry(title="test_title", domain="test") + entry2 = MockConfigEntry(title="test_title", domain="test") mock_setup_entry = AsyncMock(return_value=True) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) @@ -3313,7 +3314,19 @@ async def test_reauth(hass): assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + assert entry.entry_id != entry2.entry_id + # Check we can't start duplicate flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() - assert len(flows) == 1 + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check we can't start duplicate when the context context is different + entry.async_start_reauth(hass, {"diff": "diff"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # Check we can start a reauth for a different entry + entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 2 From 0ce301ae7ba97b73be6b22598c0e6431151d0e0f Mon Sep 17 00:00:00 2001 From: Jon Gilmore <7232986+JonGilmore@users.noreply.github.com> Date: Mon, 7 Nov 2022 21:22:03 -0600 Subject: [PATCH 293/394] Remove JonGilmore from lutron codeowners (#81727) --- CODEOWNERS | 1 - homeassistant/components/lutron/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d33cf5450386dc..db01573e8085a2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -655,7 +655,6 @@ build.json @home-assistant/supervisor /homeassistant/components/luftdaten/ @fabaff @frenck /tests/components/luftdaten/ @fabaff @frenck /homeassistant/components/lupusec/ @majuss -/homeassistant/components/lutron/ @JonGilmore /homeassistant/components/lutron_caseta/ @swails @bdraco @danaues /tests/components/lutron_caseta/ @swails @bdraco @danaues /homeassistant/components/lyric/ @timmo001 diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 7c3e66c71275c7..d46a47cf38d7ea 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": ["pylutron==0.2.8"], - "codeowners": ["@JonGilmore"], + "codeowners": [], "iot_class": "local_polling", "loggers": ["pylutron"] } From c3d4a9cd995e0d2dc7b7ce7f5f505aea65125b72 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Nov 2022 07:21:09 +0100 Subject: [PATCH 294/394] Create repairs issue if an outdated currency code is configured (#81717) * Create repairs issue if an outdated currency code is configured * Add script for updating list of currencies * Use black for formatting * Move currency codes to a separate file * Address review comments --- .pre-commit-config.yaml | 2 +- .../components/homeassistant/strings.json | 6 + homeassistant/config.py | 35 ++- homeassistant/generated/currencies.py | 290 ++++++++++++++++++ homeassistant/helpers/config_validation.py | 168 +--------- script/currencies.py | 55 ++++ tests/helpers/test_config_validation.py | 12 + tests/test_config.py | 18 +- 8 files changed, 418 insertions(+), 168 deletions(-) create mode 100644 homeassistant/generated/currencies.py create mode 100644 script/currencies.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a2e4d15482a1c..1676b60a802e9b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: hooks: - id: codespell args: - - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,iif,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa,pullrequests + - --ignore-words-list=hass,alot,bre,datas,dof,dur,ether,farenheit,hist,iff,iif,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,sur,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa,pullrequests - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 317e9d1dfcd6e5..2db00081eaafb2 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "title": "The configured currency is no longer in use", + "description": "The currency {currency} is no longer in use, please reconfigure the currency configuration." + } + }, "system_health": { "info": { "arch": "CPU Architecture", diff --git a/homeassistant/config.py b/homeassistant/config.py index e56dff4e491696..8e06c2c47a2ed0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -3,6 +3,7 @@ from collections import OrderedDict from collections.abc import Callable, Sequence +from contextlib import suppress import logging import os from pathlib import Path @@ -47,12 +48,19 @@ LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, __version__, ) -from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback +from .core import ( + DOMAIN as CONF_CORE, + ConfigSource, + HomeAssistant, + async_get_hass, + callback, +) from .exceptions import HomeAssistantError from .helpers import ( config_per_platform, config_validation as cv, extract_domain_configs, + issue_registry as ir, ) from .helpers.entity_values import EntityValues from .helpers.typing import ConfigType @@ -199,6 +207,27 @@ def _filter_bad_internal_external_urls(conf: dict) -> dict: } ) + +def _validate_currency(data: Any) -> Any: + hass = async_get_hass() + try: + return cv.currency(data) + except vol.InInvalid: + with suppress(vol.InInvalid): + currency = cv.historic_currency(data) + ir.async_create_issue( + hass, + "homeassistant", + "historic_currency", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="historic_currency", + translation_placeholders={"currency": currency}, + ) + return currency + raise + + CORE_CONFIG_SCHEMA = vol.All( CUSTOMIZE_CONFIG_SCHEMA.extend( { @@ -250,10 +279,10 @@ def _filter_bad_internal_external_urls(conf: dict) -> dict: ], _no_duplicate_auth_mfa_module, ), - # pylint: disable=no-value-for-parameter + # pylint: disable-next=no-value-for-parameter vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()), vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean, - vol.Optional(CONF_CURRENCY): cv.currency, + vol.Optional(CONF_CURRENCY): _validate_currency, } ), _filter_bad_internal_external_urls, diff --git a/homeassistant/generated/currencies.py b/homeassistant/generated/currencies.py new file mode 100644 index 00000000000000..7a5a6a31bb5412 --- /dev/null +++ b/homeassistant/generated/currencies.py @@ -0,0 +1,290 @@ +"""Automatically generated by currencies.py. + +To update, run python3 -m script.currencies +""" + +ACTIVE_CURRENCIES = { + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHF", + "CLP", + "CNY", + "COP", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HRK", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLE", + "SLL", + "SOS", + "SRD", + "SSP", + "STN", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "UYU", + "UZS", + "VED", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XOF", + "XPF", + "YER", + "ZAR", + "ZMW", + "ZWL", +} + +HISTORIC_CURRENCIES = { + "ADP", + "AFA", + "ALK", + "AOK", + "AON", + "AOR", + "ARA", + "ARP", + "ARY", + "ATS", + "AYM", + "AZM", + "BAD", + "BEC", + "BEF", + "BEL", + "BGJ", + "BGK", + "BGL", + "BOP", + "BRB", + "BRC", + "BRE", + "BRN", + "BRR", + "BUK", + "BYB", + "BYR", + "CHC", + "CSD", + "CSJ", + "CSK", + "CYP", + "DDM", + "DEM", + "ECS", + "ECV", + "EEK", + "ESA", + "ESB", + "ESP", + "FIM", + "FRF", + "GEK", + "GHC", + "GHP", + "GNE", + "GNS", + "GQE", + "GRD", + "GWE", + "GWP", + "HRD", + "IEP", + "ILP", + "ILR", + "ISJ", + "ITL", + "LAJ", + "LSM", + "LTL", + "LTT", + "LUC", + "LUF", + "LUL", + "LVL", + "LVR", + "MGF", + "MLF", + "MRO", + "MTL", + "MTP", + "MVQ", + "MXP", + "MZE", + "MZM", + "NIC", + "NLG", + "PEH", + "PEI", + "PES", + "PLZ", + "PTE", + "RHD", + "ROK", + "ROL", + "RUR", + "SDD", + "SDP", + "SIT", + "SKK", + "SRG", + "STD", + "SUR", + "TJR", + "TMM", + "TPE", + "TRL", + "UAK", + "UGS", + "UGW", + "USS", + "UYN", + "UYP", + "VEB", + "VEF", + "VNC", + "XEU", + "XFO", + "YDD", + "YUD", + "YUM", + "YUN", + "ZAL", + "ZMK", + "ZRN", + "ZRZ", + "ZWC", + "ZWD", + "ZWN", + "ZWR", +} diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 35191d7704245e..48f26c2b768130 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -88,6 +88,7 @@ ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError +from homeassistant.generated import currencies from homeassistant.util import raise_if_invalid_path, slugify as util_slugify import homeassistant.util.dt as dt_util @@ -1654,167 +1655,10 @@ def determine_script_action(action: dict[str, Any]) -> str: } -# Validate currencies adopted by countries currency = vol.In( - { - "AED", - "AFN", - "ALL", - "AMD", - "ANG", - "AOA", - "ARS", - "AUD", - "AWG", - "AZN", - "BAM", - "BBD", - "BDT", - "BGN", - "BHD", - "BIF", - "BMD", - "BND", - "BOB", - "BRL", - "BSD", - "BTN", - "BWP", - "BYN", - "BYR", - "BZD", - "CAD", - "CDF", - "CHF", - "CLP", - "CNY", - "COP", - "CRC", - "CUP", - "CVE", - "CZK", - "DJF", - "DKK", - "DOP", - "DZD", - "EGP", - "ERN", - "ETB", - "EUR", - "FJD", - "FKP", - "GBP", - "GEL", - "GHS", - "GIP", - "GMD", - "GNF", - "GTQ", - "GYD", - "HKD", - "HNL", - "HRK", - "HTG", - "HUF", - "IDR", - "ILS", - "INR", - "IQD", - "IRR", - "ISK", - "JMD", - "JOD", - "JPY", - "KES", - "KGS", - "KHR", - "KMF", - "KPW", - "KRW", - "KWD", - "KYD", - "KZT", - "LAK", - "LBP", - "LKR", - "LRD", - "LSL", - "LTL", - "LYD", - "MAD", - "MDL", - "MGA", - "MKD", - "MMK", - "MNT", - "MOP", - "MRO", - "MUR", - "MVR", - "MWK", - "MXN", - "MYR", - "MZN", - "NAD", - "NGN", - "NIO", - "NOK", - "NPR", - "NZD", - "OMR", - "PAB", - "PEN", - "PGK", - "PHP", - "PKR", - "PLN", - "PYG", - "QAR", - "RON", - "RSD", - "RUB", - "RWF", - "SAR", - "SBD", - "SCR", - "SDG", - "SEK", - "SGD", - "SHP", - "SLL", - "SOS", - "SRD", - "SSP", - "STD", - "SYP", - "SZL", - "THB", - "TJS", - "TMT", - "TND", - "TOP", - "TRY", - "TTD", - "TWD", - "TZS", - "UAH", - "UGX", - "USD", - "UYU", - "UZS", - "VEF", - "VND", - "VUV", - "WST", - "XAF", - "XCD", - "XOF", - "XPF", - "YER", - "ZAR", - "ZMK", - "ZMW", - "ZWL", - }, - msg="invalid ISO 4217 formatted currency", + currencies.ACTIVE_CURRENCIES, msg="invalid ISO 4217 formatted currency" +) + +historic_currency = vol.In( + currencies.HISTORIC_CURRENCIES, msg="invalid ISO 4217 formatted historic currency" ) diff --git a/script/currencies.py b/script/currencies.py new file mode 100644 index 00000000000000..2e538ff7c97472 --- /dev/null +++ b/script/currencies.py @@ -0,0 +1,55 @@ +"""Helper script to update currency list from the official source.""" +import pathlib + +import black +from bs4 import BeautifulSoup +import requests + +BASE = """ +\"\"\"Automatically generated by currencies.py. + +To update, run python3 -m script.currencies +\"\"\" + +ACTIVE_CURRENCIES = {{ {} }} + +HISTORIC_CURRENCIES = {{ {} }} +""".strip() + +req = requests.get( + "https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml" +) +soup = BeautifulSoup(req.content, "xml") +active_currencies = sorted( + { + x.Ccy.contents[0] + for x in soup.ISO_4217.CcyTbl.children + if x.name == "CcyNtry" + and x.Ccy + and x.CcyMnrUnts.contents[0] != "N.A." + and "IsFund" not in x.CcyNm.attrs + and x.Ccy.contents[0] != "UYW" + } +) + +req = requests.get( + "https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-three.xml" +) +soup = BeautifulSoup(req.content, "xml") +historic_currencies = sorted( + { + x.Ccy.contents[0] + for x in soup.ISO_4217.HstrcCcyTbl.children + if x.name == "HstrcCcyNtry" + and x.Ccy + and "IsFund" not in x.CcyNm.attrs + and x.Ccy.contents[0] not in active_currencies + } +) + +pathlib.Path("homeassistant/generated/currencies.py").write_text( + black.format_str( + BASE.format(repr(active_currencies)[1:-1], repr(historic_currencies)[1:-1]), + mode=black.Mode(), + ) +) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index a5d2223a3d2ac8..da9fa2cc68dfba 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1332,3 +1332,15 @@ def test_currency(): for value in ("EUR", "USD"): assert schema(value) + + +def test_historic_currency(): + """Test historic currency validator.""" + schema = vol.Schema(cv.historic_currency) + + for value in (None, "BTC", "EUR"): + with pytest.raises(vol.MultipleInvalid): + schema(value) + + for value in ("DEM", "NLG"): + assert schema(value) diff --git a/tests/test_config.py b/tests/test_config.py index 0a125d8f121a35..75ad227a641821 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -28,7 +28,7 @@ __version__, ) from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, issue_registry as ir import homeassistant.helpers.check_config as check_config from homeassistant.helpers.entity import Entity from homeassistant.loader import async_get_integration @@ -445,7 +445,7 @@ async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_stor assert hass.config.config_source is ConfigSource.STORAGE -async def test_igration_and_updating_configuration(hass, hass_storage): +async def test_migration_and_updating_configuration(hass, hass_storage): """Test updating configuration stores the new configuration.""" core_data = { "data": { @@ -1205,3 +1205,17 @@ def test_identify_config_schema(domain, schema, expected): config_util._identify_config_schema(Mock(DOMAIN=domain, CONFIG_SCHEMA=schema)) == expected ) + + +def test_core_config_schema_historic_currency(hass): + """Test core config schema.""" + config_util.CORE_CONFIG_SCHEMA( + { + "currency": "LTT", + } + ) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue("homeassistant", "historic_currency") + assert issue + assert issue.translation_placeholders == {"currency": "LTT"} From 3444d2af1aff29a09b86b7046d71cb8ba8a1c3b3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 8 Nov 2022 07:38:31 +0100 Subject: [PATCH 295/394] UniFi switch entity description (#81680) * Consolidate switch entities to one class * Move turn on/off into UnifiSwitchEntity * Add event subscription Remove storing entity for everything but legacy poe switch * Only one entity class * Improve generics naming * Rename loader to description * Improve control_fn naming * Move wrongfully placed method that should only react to dpi apps being emptied * Improve different methods * Minor renaming and sorting * Mark callbacks properly --- homeassistant/components/unifi/diagnostics.py | 1 - homeassistant/components/unifi/switch.py | 704 ++++++++---------- tests/components/unifi/test_diagnostics.py | 30 - 3 files changed, 299 insertions(+), 436 deletions(-) diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index b35fd520ab0b17..495613f3b81c25 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -95,7 +95,6 @@ async def async_get_config_entry_diagnostics( async_replace_dict_data(config_entry.as_dict(), macs_to_redact), REDACT_CONFIG ) diag["site_role"] = controller.site_role - diag["entities"] = async_replace_dict_data(controller.entities, macs_to_redact) diag["clients"] = { macs_to_redact[k]: async_redact_data( async_replace_dict_data(v.raw, macs_to_redact), REDACT_CLIENTS diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 65d0041187efc8..e63d5548ebcb73 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -7,24 +7,33 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass from typing import Any, Generic, TypeVar -from aiounifi.interfaces.api_handlers import ItemEvent +import aiounifi +from aiounifi.interfaces.api_handlers import CallbackType, ItemEvent, UnsubscribeType from aiounifi.interfaces.clients import Clients from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.ports import Ports -from aiounifi.models.client import ClientBlockRequest +from aiounifi.models.client import Client, ClientBlockRequest from aiounifi.models.device import ( DeviceSetOutletRelayRequest, DeviceSetPoePortModeRequest, ) from aiounifi.models.dpi_restriction_app import DPIRestrictionAppEnableRequest +from aiounifi.models.dpi_restriction_group import DPIRestrictionGroup from aiounifi.models.event import Event, EventKey - -from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEntity +from aiounifi.models.outlet import Outlet +from aiounifi.models.port import Port + +from homeassistant.components.switch import ( + DOMAIN, + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er @@ -37,33 +46,219 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import ( - ATTR_MANUFACTURER, - BLOCK_SWITCH, - DOMAIN as UNIFI_DOMAIN, - DPI_SWITCH, - OUTLET_SWITCH, - POE_SWITCH, -) +from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN, POE_SWITCH from .controller import UniFiController from .unifi_client import UniFiClient CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED) -T = TypeVar("T") +Data = TypeVar("Data") +Handler = TypeVar("Handler") + +Subscription = Callable[[CallbackType, ItemEvent], UnsubscribeType] + + +@callback +def async_dpi_group_is_on_fn( + api: aiounifi.Controller, dpi_group: DPIRestrictionGroup +) -> bool: + """Calculate if all apps are enabled.""" + return all( + api.dpi_apps[app_id].enabled + for app_id in dpi_group.dpiapp_ids or [] + if app_id in api.dpi_apps + ) + + +@callback +def async_sub_device_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if sub device object is disabled.""" + device_id = obj_id.partition("_")[0] + device = controller.api.devices[device_id] + return controller.available and not device.disabled + + +@callback +def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for client.""" + client = api.clients[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, obj_id)}, + default_manufacturer=client.oui, + default_name=client.name or client.hostname, + ) + + +@callback +def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for device.""" + if "_" in obj_id: # Sub device + obj_id = obj_id.partition("_")[0] + + device = api.devices[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, device.mac)}, + manufacturer=ATTR_MANUFACTURER, + model=device.model, + name=device.name or None, + sw_version=device.version, + hw_version=str(device.board_revision), + ) + + +@callback +def async_dpi_group_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for DPI group.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, f"unifi_controller_{obj_id}")}, + manufacturer=ATTR_MANUFACTURER, + model="UniFi Network", + name="UniFi Network", + ) + + +async def async_block_client_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control network access of client.""" + await api.request(ClientBlockRequest.create(obj_id, not target)) + + +async def async_dpi_group_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Enable or disable DPI group.""" + dpi_group = api.dpi_groups[obj_id] + await asyncio.gather( + *[ + api.request(DPIRestrictionAppEnableRequest.create(app_id, target)) + for app_id in dpi_group.dpiapp_ids or [] + ] + ) + + +async def async_outlet_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control outlet relay.""" + mac, _, index = obj_id.partition("_") + device = api.devices[mac] + await api.request(DeviceSetOutletRelayRequest.create(device, int(index), target)) + + +async def async_poe_port_control_fn( + api: aiounifi.Controller, obj_id: str, target: bool +) -> None: + """Control poe state.""" + mac, _, index = obj_id.partition("_") + device = api.devices[mac] + state = "auto" if target else "off" + await api.request(DeviceSetPoePortModeRequest.create(device, int(index), state)) @dataclass -class UnifiEntityLoader(Generic[T]): +class UnifiEntityLoader(Generic[Handler, Data]): """Validate and load entities from different UniFi handlers.""" allowed_fn: Callable[[UniFiController, str], bool] - entity_cls: type[UnifiBlockClientSwitch] | type[UnifiDPIRestrictionSwitch] | type[ - UnifiOutletSwitch - ] | type[UnifiPoePortSwitch] | type[UnifiDPIRestrictionSwitch] - handler_fn: Callable[[UniFiController], T] - supported_fn: Callable[[T, str], bool | None] + api_handler_fn: Callable[[aiounifi.Controller], Handler] + available_fn: Callable[[UniFiController, str], bool] + control_fn: Callable[[aiounifi.Controller, str, bool], Coroutine[Any, Any, None]] + device_info_fn: Callable[[aiounifi.Controller, str], DeviceInfo] + event_is_on: tuple[EventKey, ...] | None + event_to_subscribe: tuple[EventKey, ...] | None + is_on_fn: Callable[[aiounifi.Controller, Data], bool] + name_fn: Callable[[Data], str | None] + object_fn: Callable[[aiounifi.Controller, str], Data] + supported_fn: Callable[[aiounifi.Controller, str], bool | None] + unique_id_fn: Callable[[str], str] + + +@dataclass +class UnifiEntityDescription(SwitchEntityDescription, UnifiEntityLoader[Handler, Data]): + """Class describing UniFi switch entity.""" + + custom_subscribe: Callable[[aiounifi.Controller], Subscription] | None = None + + +ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( + UnifiEntityDescription[Clients, Client]( + key="Block client", + device_class=SwitchDeviceClass.SWITCH, + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + icon="mdi:ethernet", + allowed_fn=lambda controller, obj_id: obj_id in controller.option_block_clients, + api_handler_fn=lambda api: api.clients, + available_fn=lambda controller, obj_id: controller.available, + control_fn=async_block_client_control_fn, + device_info_fn=async_client_device_info_fn, + event_is_on=CLIENT_UNBLOCKED, + event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED, + is_on_fn=lambda api, client: not client.blocked, + name_fn=lambda client: None, + object_fn=lambda api, obj_id: api.clients[obj_id], + supported_fn=lambda api, obj_id: True, + unique_id_fn=lambda obj_id: f"block-{obj_id}", + ), + UnifiEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup]( + key="DPI restriction", + entity_category=EntityCategory.CONFIG, + icon="mdi:network", + allowed_fn=lambda controller, obj_id: controller.option_dpi_restrictions, + api_handler_fn=lambda api: api.dpi_groups, + available_fn=lambda controller, obj_id: controller.available, + control_fn=async_dpi_group_control_fn, + custom_subscribe=lambda api: api.dpi_apps.subscribe, + device_info_fn=async_dpi_group_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=async_dpi_group_is_on_fn, + name_fn=lambda group: group.name, + object_fn=lambda api, obj_id: api.dpi_groups[obj_id], + supported_fn=lambda api, obj_id: bool(api.dpi_groups[obj_id].dpiapp_ids), + unique_id_fn=lambda obj_id: obj_id, + ), + UnifiEntityDescription[Outlets, Outlet]( + key="Outlet control", + device_class=SwitchDeviceClass.OUTLET, + has_entity_name=True, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.outlets, + available_fn=async_sub_device_available_fn, + control_fn=async_outlet_control_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=lambda api, outlet: outlet.relay_state, + name_fn=lambda outlet: outlet.name, + object_fn=lambda api, obj_id: api.outlets[obj_id], + supported_fn=lambda api, obj_id: api.outlets[obj_id].has_relay, + unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", + ), + UnifiEntityDescription[Ports, Port]( + key="PoE port control", + device_class=SwitchDeviceClass.OUTLET, + entity_category=EntityCategory.CONFIG, + has_entity_name=True, + entity_registry_enabled_default=False, + icon="mdi:ethernet", + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.ports, + available_fn=async_sub_device_available_fn, + control_fn=async_poe_port_control_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + is_on_fn=lambda api, port: port.poe_mode != "off", + name_fn=lambda port: f"{port.name} PoE", + object_fn=lambda api, obj_id: api.ports[obj_id], + supported_fn=lambda api, obj_id: api.ports[obj_id].port_poe, + unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", + ), +) async def async_setup_entry( @@ -71,17 +266,9 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up switches for UniFi Network integration. - - Switches are controlling network access and switch ports with POE. - """ + """Set up switches for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - controller.entities[DOMAIN] = { - BLOCK_SWITCH: set(), - POE_SWITCH: set(), - DPI_SWITCH: set(), - OUTLET_SWITCH: set(), - } + controller.entities[DOMAIN] = {POE_SWITCH: set()} if controller.site_role != "admin": return @@ -125,20 +312,20 @@ def items_added( known_poe_clients.clear() @callback - def async_load_entities(loader: UnifiEntityLoader) -> None: + def async_load_entities(description: UnifiEntityDescription) -> None: """Load and subscribe to UniFi devices.""" entities: list[SwitchEntity] = [] - api_handler = loader.handler_fn(controller) + api_handler = description.api_handler_fn(controller.api) @callback def async_create_entity(event: ItemEvent, obj_id: str) -> None: """Create UniFi entity.""" - if not loader.allowed_fn(controller, obj_id) or not loader.supported_fn( - api_handler, obj_id - ): + if not description.allowed_fn( + controller, obj_id + ) or not description.supported_fn(controller.api, obj_id): return - entity = loader.entity_cls(obj_id, controller) + entity = UnifiSwitchEntity(obj_id, controller, description) if event == ItemEvent.ADDED: async_add_entities([entity]) return @@ -150,8 +337,8 @@ def async_create_entity(event: ItemEvent, obj_id: str) -> None: api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - for unifi_loader in UNIFI_LOADERS: - async_load_entities(unifi_loader) + for description in ENTITY_DESCRIPTIONS: + async_load_entities(description) @callback @@ -301,197 +488,104 @@ async def options_updated(self) -> None: await self.remove_item({self.client.mac}) -class UnifiBlockClientSwitch(SwitchEntity): - """Representation of a blockable client.""" +class UnifiSwitchEntity(SwitchEntity): + """Base representation of a UniFi switch.""" - _attr_device_class = SwitchDeviceClass.SWITCH - _attr_entity_category = EntityCategory.CONFIG - _attr_has_entity_name = True - _attr_icon = "mdi:ethernet" + entity_description: UnifiEntityDescription _attr_should_poll = False - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up block switch.""" - controller.entities[DOMAIN][BLOCK_SWITCH].add(obj_id) + def __init__( + self, + obj_id: str, + controller: UniFiController, + description: UnifiEntityDescription, + ) -> None: + """Set up UniFi switch entity.""" self._obj_id = obj_id self.controller = controller + self.entity_description = description self._removed = False - client = controller.api.clients[obj_id] - self._attr_available = controller.available - self._attr_is_on = not client.blocked - self._attr_unique_id = f"{BLOCK_SWITCH}-{obj_id}" - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, obj_id)}, - default_manufacturer=client.oui, - default_name=client.name or client.hostname, - ) - - async def async_added_to_hass(self) -> None: - """Entity created.""" - self.async_on_remove( - self.controller.api.clients.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - self.controller.api.events.subscribe( - self.async_event_callback, CLIENT_BLOCKED + CLIENT_UNBLOCKED - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, self.controller.signal_remove, self.remove_item - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, self.controller.signal_options_update, self.options_updated - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect object when removed.""" - self.controller.entities[DOMAIN][BLOCK_SWITCH].remove(self._obj_id) + self._attr_available = description.available_fn(controller, obj_id) + self._attr_device_info = description.device_info_fn(controller.api, obj_id) + self._attr_unique_id = description.unique_id_fn(obj_id) - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Update the clients state.""" - if event == ItemEvent.DELETED: - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - client = self.controller.api.clients[self._obj_id] - self._attr_is_on = not client.blocked - self._attr_available = self.controller.available - self.async_write_ha_state() - - @callback - def async_event_callback(self, event: Event) -> None: - """Event subscription callback.""" - if event.mac != self._obj_id: - return - if event.key in CLIENT_BLOCKED + CLIENT_UNBLOCKED: - self._attr_is_on = event.key in CLIENT_UNBLOCKED - self._attr_available = self.controller.available - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) + obj = description.object_fn(self.controller.api, obj_id) + self._attr_is_on = description.is_on_fn(controller.api, obj) + self._attr_name = description.name_fn(obj) async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on connectivity for client.""" - await self.controller.api.request( - ClientBlockRequest.create(self._obj_id, False) + """Turn on switch.""" + await self.entity_description.control_fn( + self.controller.api, self._obj_id, True ) async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off connectivity for client.""" - await self.controller.api.request(ClientBlockRequest.create(self._obj_id, True)) - - @property - def icon(self) -> str: - """Return the icon to use in the frontend.""" - if not self.is_on: - return "mdi:network-off" - return "mdi:network" - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if self._obj_id not in self.controller.option_block_clients: - await self.remove_item({self._obj_id}) - - async def remove_item(self, keys: set) -> None: - """Remove entity if key is part of set.""" - if self._obj_id not in keys or self._removed: - return - self._removed = True - if self.registry_entry: - er.async_get(self.hass).async_remove(self.entity_id) - else: - await self.async_remove(force_remove=True) - - -class UnifiDPIRestrictionSwitch(SwitchEntity): - """Representation of a DPI restriction group.""" - - _attr_entity_category = EntityCategory.CONFIG - - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up dpi switch.""" - controller.entities[DOMAIN][DPI_SWITCH].add(obj_id) - self._obj_id = obj_id - self.controller = controller - - dpi_group = controller.api.dpi_groups[obj_id] - self._known_app_ids = dpi_group.dpiapp_ids - - self._attr_available = controller.available - self._attr_is_on = self.calculate_enabled() - self._attr_name = dpi_group.name - self._attr_unique_id = dpi_group.id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"unifi_controller_{obj_id}")}, - manufacturer=ATTR_MANUFACTURER, - model="UniFi Network", - name="UniFi Network", + """Turn off switch.""" + await self.entity_description.control_fn( + self.controller.api, self._obj_id, False ) async def async_added_to_hass(self) -> None: - """Register callback to known apps.""" + """Register callbacks.""" + description = self.entity_description + handler = description.api_handler_fn(self.controller.api) self.async_on_remove( - self.controller.api.dpi_groups.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - self.controller.api.dpi_apps.subscribe( - self.async_signalling_callback, ItemEvent.CHANGED - ), + handler.subscribe( + self.async_signalling_callback, + ) ) self.async_on_remove( async_dispatcher_connect( - self.hass, self.controller.signal_remove, self.remove_item + self.hass, + self.controller.signal_reachable, + self.async_signal_reachable_callback, ) ) self.async_on_remove( async_dispatcher_connect( - self.hass, self.controller.signal_options_update, self.options_updated + self.hass, + self.controller.signal_options_update, + self.options_updated, ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, + self.controller.signal_remove, + self.remove_item, ) ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect object when removed.""" - self.controller.entities[DOMAIN][DPI_SWITCH].remove(self._obj_id) + if description.event_to_subscribe is not None: + self.async_on_remove( + self.controller.api.events.subscribe( + self.async_event_callback, + description.event_to_subscribe, + ) + ) + if description.custom_subscribe is not None: + self.async_on_remove( + description.custom_subscribe(self.controller.api)( + self.async_signalling_callback, ItemEvent.CHANGED + ), + ) @callback def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Object has new event.""" - if event == ItemEvent.DELETED: + """Update the switch state.""" + if event == ItemEvent.DELETED and obj_id == self._obj_id: self.hass.async_create_task(self.remove_item({self._obj_id})) return - dpi_group = self.controller.api.dpi_groups[self._obj_id] - if not dpi_group.dpiapp_ids: + description = self.entity_description + if not description.supported_fn(self.controller.api, self._obj_id): self.hass.async_create_task(self.remove_item({self._obj_id})) return - self._attr_available = self.controller.available - self._attr_is_on = self.calculate_enabled() + obj = description.object_fn(self.controller.api, self._obj_id) + self._attr_is_on = description.is_on_fn(self.controller.api, obj) + self._attr_available = description.available_fn(self.controller, self._obj_id) self.async_write_ha_state() @callback @@ -499,232 +593,32 @@ def async_signal_reachable_callback(self) -> None: """Call when controller connection state change.""" self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - @property - def icon(self): - """Return the icon to use in the frontend.""" - if self.is_on: - return "mdi:network" - return "mdi:network-off" - - def calculate_enabled(self) -> bool: - """Calculate if all apps are enabled.""" - dpi_group = self.controller.api.dpi_groups[self._obj_id] - return all( - self.controller.api.dpi_apps[app_id].enabled - for app_id in dpi_group.dpiapp_ids - if app_id in self.controller.api.dpi_apps - ) + @callback + def async_event_callback(self, event: Event) -> None: + """Event subscription callback.""" + if event.mac != self._obj_id: + return - async def async_turn_on(self, **kwargs: Any) -> None: - """Restrict access of apps related to DPI group.""" - dpi_group = self.controller.api.dpi_groups[self._obj_id] - return await asyncio.gather( - *[ - self.controller.api.request( - DPIRestrictionAppEnableRequest.create(app_id, True) - ) - for app_id in dpi_group.dpiapp_ids - ] - ) + description = self.entity_description + assert isinstance(description.event_to_subscribe, tuple) + assert isinstance(description.event_is_on, tuple) - async def async_turn_off(self, **kwargs: Any) -> None: - """Remove restriction of apps related to DPI group.""" - dpi_group = self.controller.api.dpi_groups[self._obj_id] - return await asyncio.gather( - *[ - self.controller.api.request( - DPIRestrictionAppEnableRequest.create(app_id, False) - ) - for app_id in dpi_group.dpiapp_ids - ] - ) + if event.key in description.event_to_subscribe: + self._attr_is_on = event.key in description.event_is_on + self._attr_available = description.available_fn(self.controller, self._obj_id) + self.async_write_ha_state() async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" - if not self.controller.option_dpi_restrictions: - await self.remove_item({self._attr_unique_id}) + if not self.entity_description.allowed_fn(self.controller, self._obj_id): + await self.remove_item({self._obj_id}) async def remove_item(self, keys: set) -> None: - """Remove entity if key is part of set.""" - if self._attr_unique_id not in keys: + """Remove entity if object ID is part of set.""" + if self._obj_id not in keys or self._removed: return - + self._removed = True if self.registry_entry: er.async_get(self.hass).async_remove(self.entity_id) else: await self.async_remove(force_remove=True) - - -class UnifiOutletSwitch(SwitchEntity): - """Representation of a outlet relay.""" - - _attr_device_class = SwitchDeviceClass.OUTLET - _attr_has_entity_name = True - _attr_should_poll = False - - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up UniFi Network entity base.""" - self._device_mac, index = obj_id.split("_", 1) - self._index = int(index) - self._obj_id = obj_id - self.controller = controller - - outlet = self.controller.api.outlets[self._obj_id] - self._attr_name = outlet.name - self._attr_is_on = outlet.relay_state - self._attr_unique_id = f"{self._device_mac}-outlet-{index}" - - device = self.controller.api.devices[self._device_mac] - self._attr_available = controller.available and not device.disabled - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=device.board_revision, - ) - - async def async_added_to_hass(self) -> None: - """Entity created.""" - self.async_on_remove( - self.controller.api.outlets.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Object has new event.""" - device = self.controller.api.devices[self._device_mac] - outlet = self.controller.api.outlets[self._obj_id] - self._attr_available = self.controller.available and not device.disabled - self._attr_is_on = outlet.relay_state - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Enable outlet relay.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetOutletRelayRequest.create(device, self._index, True) - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Disable outlet relay.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetOutletRelayRequest.create(device, self._index, False) - ) - - -class UnifiPoePortSwitch(SwitchEntity): - """Representation of a Power-over-Ethernet source port on an UniFi device.""" - - _attr_device_class = SwitchDeviceClass.OUTLET - _attr_entity_category = EntityCategory.CONFIG - _attr_entity_registry_enabled_default = False - _attr_has_entity_name = True - _attr_icon = "mdi:ethernet" - _attr_should_poll = False - - def __init__(self, obj_id: str, controller: UniFiController) -> None: - """Set up UniFi Network entity base.""" - self._device_mac, index = obj_id.split("_", 1) - self._index = int(index) - self._obj_id = obj_id - self.controller = controller - - port = self.controller.api.ports[self._obj_id] - self._attr_name = f"{port.name} PoE" - self._attr_is_on = port.poe_mode != "off" - self._attr_unique_id = f"{self._device_mac}-poe-{index}" - - device = self.controller.api.devices[self._device_mac] - self._attr_available = controller.available and not device.disabled - self._attr_device_info = DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=device.board_revision, - ) - - async def async_added_to_hass(self) -> None: - """Entity created.""" - self.async_on_remove( - self.controller.api.ports.subscribe(self.async_signalling_callback) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Object has new event.""" - device = self.controller.api.devices[self._device_mac] - port = self.controller.api.ports[self._obj_id] - self._attr_available = self.controller.available and not device.disabled - self._attr_is_on = port.poe_mode != "off" - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Enable POE for client.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetPoePortModeRequest.create(device, self._index, "auto") - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Disable POE for client.""" - device = self.controller.api.devices[self._device_mac] - await self.controller.api.request( - DeviceSetPoePortModeRequest.create(device, self._index, "off") - ) - - -UNIFI_LOADERS: tuple[UnifiEntityLoader, ...] = ( - UnifiEntityLoader[Clients]( - allowed_fn=lambda controller, obj_id: obj_id in controller.option_block_clients, - entity_cls=UnifiBlockClientSwitch, - handler_fn=lambda contrlr: contrlr.api.clients, - supported_fn=lambda handler, obj_id: True, - ), - UnifiEntityLoader[DPIRestrictionGroups]( - allowed_fn=lambda controller, obj_id: controller.option_dpi_restrictions, - entity_cls=UnifiDPIRestrictionSwitch, - handler_fn=lambda controller: controller.api.dpi_groups, - supported_fn=lambda handler, obj_id: bool(handler[obj_id].dpiapp_ids), - ), - UnifiEntityLoader[Outlets]( - allowed_fn=lambda controller, obj_id: True, - entity_cls=UnifiOutletSwitch, - handler_fn=lambda controller: controller.api.outlets, - supported_fn=lambda handler, obj_id: handler[obj_id].has_relay, - ), - UnifiEntityLoader[Ports]( - allowed_fn=lambda controller, obj_id: True, - entity_cls=UnifiPoePortSwitch, - handler_fn=lambda controller: controller.api.ports, - supported_fn=lambda handler, obj_id: handler[obj_id].port_poe, - ), -) diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 6584b94729354c..9de0e4b6154c4d 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -6,16 +6,6 @@ CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, ) -from homeassistant.components.unifi.device_tracker import CLIENT_TRACKER, DEVICE_TRACKER -from homeassistant.components.unifi.sensor import RX_SENSOR, TX_SENSOR, UPTIME_SENSOR -from homeassistant.components.unifi.switch import ( - BLOCK_SWITCH, - DPI_SWITCH, - OUTLET_SWITCH, - POE_SWITCH, -) -from homeassistant.components.unifi.update import DEVICE_UPDATE -from homeassistant.const import Platform from .test_controller import setup_unifi_integration @@ -146,26 +136,6 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): "version": 1, }, "site_role": "admin", - "entities": { - str(Platform.DEVICE_TRACKER): { - CLIENT_TRACKER: ["00:00:00:00:00:00"], - DEVICE_TRACKER: ["00:00:00:00:00:01"], - }, - str(Platform.SENSOR): { - RX_SENSOR: ["00:00:00:00:00:00"], - TX_SENSOR: ["00:00:00:00:00:00"], - UPTIME_SENSOR: ["00:00:00:00:00:00"], - }, - str(Platform.SWITCH): { - BLOCK_SWITCH: ["00:00:00:00:00:00"], - DPI_SWITCH: ["5f976f4ae3c58f018ec7dff6"], - POE_SWITCH: ["00:00:00:00:00:00"], - OUTLET_SWITCH: [], - }, - str(Platform.UPDATE): { - DEVICE_UPDATE: ["00:00:00:00:00:01"], - }, - }, "clients": { "00:00:00:00:00:00": { "blocked": False, From 23bed25e5206b1ec16c241e1825aad12752e268e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 8 Nov 2022 07:48:54 +0100 Subject: [PATCH 296/394] Remove old UniFi POE client implementation (#81749) Remove all references to POE client implementation --- homeassistant/components/unifi/__init__.py | 22 ++ homeassistant/components/unifi/config_flow.py | 6 - homeassistant/components/unifi/const.py | 3 - homeassistant/components/unifi/controller.py | 10 +- homeassistant/components/unifi/switch.py | 185 +--------- tests/components/unifi/test_config_flow.py | 3 - tests/components/unifi/test_switch.py | 324 +++--------------- 7 files changed, 66 insertions(+), 487 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 84540f7bea414d..e37e89b3da5d72 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -6,6 +6,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType @@ -36,6 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up the UniFi Network integration.""" hass.data.setdefault(UNIFI_DOMAIN, {}) + # Removal of legacy PoE control was introduced with 2022.12 + async_remove_poe_client_entities(hass, config_entry) + # Flat configuration was introduced with 2021.3 await async_flatten_entry_data(hass, config_entry) @@ -82,6 +86,24 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return await controller.async_reset() +@callback +def async_remove_poe_client_entities( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Remove PoE client entities.""" + ent_reg = er.async_get(hass) + + entity_ids_to_be_removed = [ + entry.entity_id + for entry in ent_reg.entities.values() + if entry.config_entry_id == config_entry.entry_id + and entry.unique_id.startswith("poe-") + ] + + for entity_id in entity_ids_to_be_removed: + ent_reg.async_remove(entity_id) + + async def async_flatten_entry_data( hass: HomeAssistant, config_entry: ConfigEntry ) -> None: diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 4944dd91296222..caf256ded205e5 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -37,14 +37,12 @@ CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, - CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, DEFAULT_DPI_RESTRICTIONS, - DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) from .controller import UniFiController, get_unifi_controller @@ -396,10 +394,6 @@ async def async_step_client_control( vol.Optional( CONF_BLOCK_CLIENT, default=selected_clients_to_block ): cv.multi_select(clients_to_block), - vol.Optional( - CONF_POE_CLIENTS, - default=self.options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS), - ): bool, vol.Optional( CONF_DPI_RESTRICTIONS, default=self.options.get( diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index bf0aaef45ddcd5..85f744e481f1fd 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -25,7 +25,6 @@ CONF_DETECTION_TIME = "detection_time" CONF_DPI_RESTRICTIONS = "dpi_restrictions" CONF_IGNORE_WIRED_BUG = "ignore_wired_bug" -CONF_POE_CLIENTS = "poe_clients" CONF_TRACK_CLIENTS = "track_clients" CONF_TRACK_DEVICES = "track_devices" CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" @@ -35,7 +34,6 @@ DEFAULT_ALLOW_UPTIME_SENSORS = False DEFAULT_DPI_RESTRICTIONS = True DEFAULT_IGNORE_WIRED_BUG = False -DEFAULT_POE_CLIENTS = True DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True DEFAULT_TRACK_WIRED_CLIENTS = True @@ -45,5 +43,4 @@ BLOCK_SWITCH = "block" DPI_SWITCH = "dpi" -POE_SWITCH = "poe" OUTLET_SWITCH = "outlet" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index c421cb5391a891..8aae95bda41586 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -44,7 +44,6 @@ CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, - CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, @@ -55,14 +54,12 @@ DEFAULT_DETECTION_TIME, DEFAULT_DPI_RESTRICTIONS, DEFAULT_IGNORE_WIRED_BUG, - DEFAULT_POE_CLIENTS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, LOGGER, PLATFORMS, - POE_SWITCH, UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect @@ -140,8 +137,6 @@ def load_config_entry_options(self): # Client control options - # Config entry option to control poe clients. - self.option_poe_clients = options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS) # Config entry option with list of clients to control network access. self.option_block_clients = options.get(CONF_BLOCK_CLIENT, []) # Config entry option to control DPI restriction groups. @@ -305,9 +300,8 @@ async def initialize(self): ): if entry.domain == Platform.DEVICE_TRACKER: mac = entry.unique_id.split("-", 1)[0] - elif entry.domain == Platform.SWITCH and ( - entry.unique_id.startswith(BLOCK_SWITCH) - or entry.unique_id.startswith(POE_SWITCH) + elif entry.domain == Platform.SWITCH and entry.unique_id.startswith( + BLOCK_SWITCH ): mac = entry.unique_id.split("-", 1)[1] else: diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index e63d5548ebcb73..5a88caca807887 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -44,11 +44,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity -from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN, POE_SWITCH +from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .controller import UniFiController -from .unifi_client import UniFiClient CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED) @@ -268,49 +266,15 @@ async def async_setup_entry( ) -> None: """Set up switches for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - controller.entities[DOMAIN] = {POE_SWITCH: set()} if controller.site_role != "admin": return - # Store previously known POE control entities in case their POE are turned off. - known_poe_clients = [] - entity_registry = er.async_get(hass) - for entry in er.async_entries_for_config_entry( - entity_registry, config_entry.entry_id - ): - - if not entry.unique_id.startswith(POE_SWITCH): - continue - - mac = entry.unique_id.replace(f"{POE_SWITCH}-", "") - if mac not in controller.api.clients: - continue - - known_poe_clients.append(mac) - for mac in controller.option_block_clients: if mac not in controller.api.clients and mac in controller.api.clients_all: client = controller.api.clients_all[mac] controller.api.clients.process_raw([client.raw]) - @callback - def items_added( - clients: set = controller.api.clients, - devices: set = controller.api.devices, - ) -> None: - """Update the values of the controller.""" - if controller.option_poe_clients: - add_poe_entities(controller, async_add_entities, clients, known_poe_clients) - - for signal in (controller.signal_update, controller.signal_options_update): - config_entry.async_on_unload( - async_dispatcher_connect(hass, signal, items_added) - ) - - items_added() - known_poe_clients.clear() - @callback def async_load_entities(description: UnifiEntityDescription) -> None: """Load and subscribe to UniFi devices.""" @@ -341,153 +305,6 @@ def async_create_entity(event: ItemEvent, obj_id: str) -> None: async_load_entities(description) -@callback -def add_poe_entities(controller, async_add_entities, clients, known_poe_clients): - """Add new switch entities from the controller.""" - switches = [] - - devices = controller.api.devices - - for mac in clients: - if mac in controller.entities[DOMAIN][POE_SWITCH]: - continue - - client = controller.api.clients[mac] - - # Try to identify new clients powered by POE. - # Known POE clients have been created in previous HASS sessions. - # If port_poe is None the port does not support POE - # If poe_enable is False we can't know if a POE client is available for control. - if mac not in known_poe_clients and ( - mac in controller.wireless_clients - or client.switch_mac not in devices - or not devices[client.switch_mac].ports[client.switch_port].port_poe - or not devices[client.switch_mac].ports[client.switch_port].poe_enable - or controller.mac == client.mac - ): - continue - - # Multiple POE-devices on same port means non UniFi POE driven switch - multi_clients_on_port = False - for client2 in controller.api.clients.values(): - - if mac in known_poe_clients: - break - - if ( - client2.is_wired - and client.mac != client2.mac - and client.switch_mac == client2.switch_mac - and client.switch_port == client2.switch_port - ): - multi_clients_on_port = True - break - - if multi_clients_on_port: - continue - - switches.append(UniFiPOEClientSwitch(client, controller)) - - async_add_entities(switches) - - -class UniFiPOEClientSwitch(UniFiClient, SwitchEntity, RestoreEntity): - """Representation of a client that uses POE.""" - - DOMAIN = DOMAIN - TYPE = POE_SWITCH - - _attr_entity_category = EntityCategory.CONFIG - - def __init__(self, client, controller): - """Set up POE switch.""" - super().__init__(client, controller) - - self.poe_mode = None - if client.switch_port and self.port.poe_mode != "off": - self.poe_mode = self.port.poe_mode - - async def async_added_to_hass(self) -> None: - """Call when entity about to be added to Home Assistant.""" - await super().async_added_to_hass() - - if self.poe_mode: # POE is enabled and client in a known state - return - - if (state := await self.async_get_last_state()) is None: - return - - self.poe_mode = state.attributes.get("poe_mode") - - if not self.client.switch_mac: - self.client.raw["sw_mac"] = state.attributes.get("switch") - - if not self.client.switch_port: - self.client.raw["sw_port"] = state.attributes.get("port") - - @property - def is_on(self): - """Return true if POE is active.""" - return self.port.poe_mode != "off" - - @property - def available(self) -> bool: - """Return if switch is available. - - Poe_mode None means its POE state is unknown. - Sw_mac unavailable means restored client. - """ - return ( - self.poe_mode is not None - and self.controller.available - and self.client.switch_port - and self.client.switch_mac - and self.client.switch_mac in self.controller.api.devices - ) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Enable POE for client.""" - await self.controller.api.request( - DeviceSetPoePortModeRequest.create( - self.device, self.client.switch_port, self.poe_mode - ) - ) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Disable POE for client.""" - await self.controller.api.request( - DeviceSetPoePortModeRequest.create( - self.device, self.client.switch_port, "off" - ) - ) - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - attributes = { - "power": self.port.poe_power, - "switch": self.client.switch_mac, - "port": self.client.switch_port, - "poe_mode": self.poe_mode, - } - return attributes - - @property - def device(self): - """Shortcut to the switch that client is connected to.""" - return self.controller.api.devices[self.client.switch_mac] - - @property - def port(self): - """Shortcut to the switch port that client is connected to.""" - return self.device.ports[self.client.switch_port] - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.controller.option_poe_clients: - await self.remove_item({self.client.mac}) - - class UnifiSwitchEntity(SwitchEntity): """Base representation of a UniFi switch.""" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index a1f6f3d4b02531..078c068c8ed0de 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -16,7 +16,6 @@ CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, - CONF_POE_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, @@ -473,7 +472,6 @@ async def test_advanced_option_flow(hass, aioclient_mock): result["flow_id"], user_input={ CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], - CONF_POE_CLIENTS: False, CONF_DPI_RESTRICTIONS: False, }, ) @@ -498,7 +496,6 @@ async def test_advanced_option_flow(hass, aioclient_mock): CONF_SSID_FILTER: ["SSID 1", "SSID 2_IOT", "SSID 3"], CONF_DETECTION_TIME: 100, CONF_IGNORE_WIRED_BUG: False, - CONF_POE_CLIENTS: False, CONF_DPI_RESTRICTIONS: False, CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], CONF_ALLOW_BANDWIDTH_SENSORS: True, diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index e6357b031725b0..5178e3dda37429 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -6,7 +6,7 @@ from aiounifi.models.message import MessageKey from aiounifi.websocket import WebsocketState -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.components.switch import ( DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -16,12 +16,10 @@ from homeassistant.components.unifi.const import ( CONF_BLOCK_CLIENT, CONF_DPI_RESTRICTIONS, - CONF_POE_CLIENTS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, DOMAIN as UNIFI_DOMAIN, ) -from homeassistant.components.unifi.switch import POE_SWITCH from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -38,13 +36,12 @@ from .test_controller import ( CONTROLLER_HOST, - DEFAULT_CONFIG_ENTRY_ID, DESCRIPTION, ENTRY_CONFIG, setup_unifi_integration, ) -from tests.common import async_fire_time_changed, mock_restore_cache +from tests.common import async_fire_time_changed CLIENT_1 = { "hostname": "client_1", @@ -636,23 +633,14 @@ async def test_switches(hass, aioclient_mock): CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, }, - clients_response=[CLIENT_1, CLIENT_4], - devices_response=[DEVICE_1], + clients_response=[CLIENT_4], clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1], dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 - - switch_1 = hass.states.get("switch.poe_client_1") - assert switch_1 is not None - assert switch_1.state == "on" - assert switch_1.attributes["power"] == "2.56" - assert switch_1.attributes[SWITCH_DOMAIN] == "10:00:00:00:01:01" - assert switch_1.attributes["port"] == 1 - assert switch_1.attributes["poe_mode"] == "auto" + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 switch_4 = hass.states.get("switch.poe_client_4") assert switch_4 is None @@ -671,11 +659,7 @@ async def test_switches(hass, aioclient_mock): assert dpi_switch.attributes["icon"] == "mdi:network" ent_reg = er.async_get(hass) - for entry_id in ( - "switch.poe_client_1", - "switch.block_client_1", - "switch.block_media_streaming", - ): + for entry_id in ("switch.block_client_1", "switch.block_media_streaming"): assert ent_reg.async_get(entry_id).entity_category is EntityCategory.CONFIG # Block and unblock client @@ -729,7 +713,7 @@ async def test_switches(hass, aioclient_mock): # Make sure no duplicates arise on generic signal update async_dispatcher_send(hass, controller.signal_update) await hass.async_block_till_done() - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 4 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket): @@ -738,24 +722,21 @@ async def test_remove_switches(hass, aioclient_mock, mock_unifi_websocket): hass, aioclient_mock, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, - clients_response=[CLIENT_1, UNBLOCKED], - devices_response=[DEVICE_1], + clients_response=[UNBLOCKED], dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 3 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - assert hass.states.get("switch.poe_client_1") is not None assert hass.states.get("switch.block_client_2") is not None assert hass.states.get("switch.block_media_streaming") is not None - mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[CLIENT_1, UNBLOCKED]) + mock_unifi_websocket(message=MessageKey.CLIENT_REMOVED, data=[UNBLOCKED]) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - assert hass.states.get("switch.poe_client_1") is None assert hass.states.get("switch.block_client_2") is None assert hass.states.get("switch.block_media_streaming") is not None @@ -1089,273 +1070,20 @@ async def test_option_remove_switches(hass, aioclient_mock): CONF_TRACK_DEVICES: False, }, clients_response=[CLIENT_1], - devices_response=[DEVICE_1], dpigroup_response=DPI_GROUPS, dpiapp_response=DPI_APPS, ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 + assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 # Disable DPI Switches hass.config_entries.async_update_entry( config_entry, - options={CONF_DPI_RESTRICTIONS: False, CONF_POE_CLIENTS: False}, + options={CONF_DPI_RESTRICTIONS: False}, ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0 -async def test_new_client_discovered_on_poe_control( - hass, aioclient_mock, mock_unifi_websocket -): - """Test if 2nd update has a new client.""" - config_entry = await setup_unifi_integration( - hass, - aioclient_mock, - options={CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False}, - clients_response=[CLIENT_1], - devices_response=[DEVICE_1], - ) - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - mock_unifi_websocket(message=MessageKey.CLIENT, data=CLIENT_2) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - mock_unifi_websocket(message=MessageKey.EVENT, data=EVENT_CLIENT_2_CONNECTED) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - switch_2 = hass.states.get("switch.poe_client_2") - assert switch_2 is not None - - aioclient_mock.put( - f"https://{controller.host}:1234/api/s/{controller.site}/rest/device/mock-id", - ) - - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True - ) - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 - assert aioclient_mock.call_count == 11 - assert aioclient_mock.mock_calls[10][2] == { - "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] - } - - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True - ) - assert aioclient_mock.call_count == 12 - assert aioclient_mock.mock_calls[11][2] == { - "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"}] - } - - -async def test_ignore_multiple_poe_clients_on_same_port(hass, aioclient_mock): - """Ignore when there are multiple POE driven clients on same port. - - If there is a non-UniFi switch powered by POE, - clients will be transparently marked as having POE as well. - """ - await setup_unifi_integration( - hass, - aioclient_mock, - clients_response=POE_SWITCH_CLIENTS, - devices_response=[DEVICE_1], - ) - - switch_1 = hass.states.get("switch.poe_client_1") - switch_2 = hass.states.get("switch.poe_client_2") - assert switch_1 is None - assert switch_2 is None - - -async def test_restore_client_succeed(hass, aioclient_mock): - """Test that RestoreEntity works as expected.""" - POE_DEVICE = { - "device_id": "12345", - "ip": "1.0.1.1", - "mac": "00:00:00:00:01:01", - "last_seen": 1562600145, - "model": "US16P150", - "name": "POE Switch", - "port_overrides": [ - { - "poe_mode": "off", - "port_idx": 1, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - } - ], - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "op_mode": "switch", - "poe_caps": 7, - "poe_class": "Unknown", - "poe_current": "0.00", - "poe_enable": False, - "poe_good": False, - "poe_mode": "off", - "poe_power": "0.00", - "poe_voltage": "0.00", - "port_idx": 1, - "port_poe": True, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - "up": False, - }, - ], - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - POE_CLIENT = { - "hostname": "poe_client", - "ip": "1.0.0.1", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - "name": "POE Client", - "oui": "Producer", - } - - fake_state = core.State( - "switch.poe_client", - "off", - { - "power": "0.00", - "switch": POE_DEVICE["mac"], - "port": 1, - "poe_mode": "auto", - }, - ) - mock_restore_cache(hass, (fake_state,)) - - config_entry = config_entries.ConfigEntry( - version=1, - domain=UNIFI_DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - options={}, - entry_id=DEFAULT_CONFIG_ENTRY_ID, - ) - - registry = er.async_get(hass) - registry.async_get_or_create( - SWITCH_DOMAIN, - UNIFI_DOMAIN, - f'{POE_SWITCH}-{POE_CLIENT["mac"]}', - suggested_object_id=POE_CLIENT["hostname"], - config_entry=config_entry, - ) - - await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, - clients_response=[], - devices_response=[POE_DEVICE], - clients_all_response=[POE_CLIENT], - ) - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - poe_client = hass.states.get("switch.poe_client") - assert poe_client.state == "off" - - -async def test_restore_client_no_old_state(hass, aioclient_mock): - """Test that RestoreEntity without old state makes entity unavailable.""" - POE_DEVICE = { - "device_id": "12345", - "ip": "1.0.1.1", - "mac": "00:00:00:00:01:01", - "last_seen": 1562600145, - "model": "US16P150", - "name": "POE Switch", - "port_overrides": [ - { - "poe_mode": "off", - "port_idx": 1, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - } - ], - "port_table": [ - { - "media": "GE", - "name": "Port 1", - "op_mode": "switch", - "poe_caps": 7, - "poe_class": "Unknown", - "poe_current": "0.00", - "poe_enable": False, - "poe_good": False, - "poe_mode": "off", - "poe_power": "0.00", - "poe_voltage": "0.00", - "port_idx": 1, - "port_poe": True, - "portconf_id": "5f3edd2aba4cc806a19f2db2", - "up": False, - }, - ], - "state": 1, - "type": "usw", - "version": "4.0.42.10433", - } - POE_CLIENT = { - "hostname": "poe_client", - "ip": "1.0.0.1", - "is_wired": True, - "last_seen": 1562600145, - "mac": "00:00:00:00:00:01", - "name": "POE Client", - "oui": "Producer", - } - - config_entry = config_entries.ConfigEntry( - version=1, - domain=UNIFI_DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - options={}, - entry_id=DEFAULT_CONFIG_ENTRY_ID, - ) - - registry = er.async_get(hass) - registry.async_get_or_create( - SWITCH_DOMAIN, - UNIFI_DOMAIN, - f'{POE_SWITCH}-{POE_CLIENT["mac"]}', - suggested_object_id=POE_CLIENT["hostname"], - config_entry=config_entry, - ) - - await setup_unifi_integration( - hass, - aioclient_mock, - options={ - CONF_TRACK_CLIENTS: False, - CONF_TRACK_DEVICES: False, - }, - clients_response=[], - devices_response=[POE_DEVICE], - clients_all_response=[POE_CLIENT], - ) - - assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1 - - poe_client = hass.states.get("switch.poe_client") - assert poe_client.state == "unavailable" # self.poe_mode is None - - async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket): """Test the update_items function with some clients.""" config_entry = await setup_unifi_integration( @@ -1447,3 +1175,33 @@ async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket): mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) await hass.async_block_till_done() assert hass.states.get("switch.mock_name_port_1_poe").state == STATE_OFF + + +async def test_remove_poe_client_switches(hass, aioclient_mock): + """Test old PoE client switches are removed.""" + + config_entry = config_entries.ConfigEntry( + version=1, + domain=UNIFI_DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + options={}, + entry_id="1", + ) + + ent_reg = er.async_get(hass) + ent_reg.async_get_or_create( + SWITCH_DOMAIN, + UNIFI_DOMAIN, + "poe-123", + config_entry=config_entry, + ) + + await setup_unifi_integration(hass, aioclient_mock) + + assert not [ + entry + for entry in ent_reg.entities.values() + if entry.config_entry_id == config_entry.entry_id + ] From 88faf33cb86ba4bebdf0e8a6199969d45071f76c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 09:17:03 +0100 Subject: [PATCH 297/394] Improve type hints for MQTT climate (#81396) * Improve typing climate * Move climate type hints to class level * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * remove stale command after applying suggestions * cleanup * Update homeassistant/components/mqtt/climate.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/mqtt/climate.py | 125 ++++++++++++++--------- 1 file changed, 77 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index e46c8e31565fe9..ef5a25d959b5f0 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,6 +1,7 @@ """Support for MQTT climate devices.""" from __future__ import annotations +from collections.abc import Callable import functools import logging from typing import Any @@ -41,6 +42,7 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -54,7 +56,13 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -197,9 +205,9 @@ ) -def valid_preset_mode_configuration(config): +def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: """Validate that the preset mode reset payload is not one of the preset modes.""" - if PRESET_NONE in config.get(CONF_PRESET_MODES_LIST): + if PRESET_NONE in config[CONF_PRESET_MODES_LIST]: raise ValueError("preset_modes must not include preset mode 'none'") return config @@ -359,8 +367,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT climate devices.""" async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) @@ -372,22 +380,28 @@ class MqttClimate(MqttEntity, ClimateEntity): _entity_id_format = climate.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED - def __init__(self, hass, config, config_entry, discovery_data): + _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] + _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] + _feature_preset_mode: bool + _optimistic_preset_mode: bool + _topic: dict[str, Any] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the climate device.""" - self._topic = None - self._value_templates = None - self._command_templates = None - self._feature_preset_mode = False - self._optimistic_preset_mode = None - MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._attr_hvac_modes = config[CONF_MODE_LIST] self._attr_min_temp = config[CONF_TEMP_MIN] @@ -438,7 +452,7 @@ def _setup_from_config(self, config): self._attr_is_aux_heat = False - value_templates = {} + value_templates: dict[str, Template | None] = {} for key in VALUE_TEMPLATE_KEYS: value_templates[key] = None if CONF_VALUE_TEMPLATE in config: @@ -455,14 +469,12 @@ def _setup_from_config(self, config): for key, template in value_templates.items() } - command_templates = {} + self._command_templates = {} for key in COMMAND_TEMPLATE_KEYS: - command_templates[key] = MqttCommandTemplate( + self._command_templates[key] = MqttCommandTemplate( config.get(key), entity=self ).async_render - self._command_templates = command_templates - support: int = 0 if (self._topic[CONF_TEMP_STATE_TOPIC] is not None) or ( self._topic[CONF_TEMP_COMMAND_TOPIC] is not None @@ -498,12 +510,16 @@ def _setup_from_config(self, config): support |= ClimateEntityFeature.AUX_HEAT self._attr_supported_features = support - def _prepare_subscribe_topics(self): # noqa: C901 + def _prepare_subscribe_topics(self) -> None: # noqa: C901 """(Re)Subscribe to topics.""" - topics = {} - qos = self._config[CONF_QOS] - - def add_subscription(topics, topic, msg_callback): + topics: dict[str, dict[str, Any]] = {} + qos: int = self._config[CONF_QOS] + + def add_subscription( + topics: dict[str, dict[str, Any]], + topic: str, + msg_callback: Callable[[ReceiveMessage], None], + ) -> None: if self._topic[topic] is not None: topics[topic] = { "topic": self._topic[topic], @@ -512,13 +528,15 @@ def add_subscription(topics, topic, msg_callback): "encoding": self._config[CONF_ENCODING] or None, } - def render_template(msg, template_name): + def render_template( + msg: ReceiveMessage, template_name: str + ) -> ReceivePayloadType: template = self._value_templates[template_name] return template(msg.payload) @callback @log_messages(self.hass, self.entity_id) - def handle_action_received(msg): + def handle_action_received(msg: ReceiveMessage) -> None: """Handle receiving action via MQTT.""" payload = render_template(msg, CONF_ACTION_TEMPLATE) if not payload or payload == PAYLOAD_NONE: @@ -529,7 +547,7 @@ def handle_action_received(msg): ) return try: - self._attr_hvac_action = HVACAction(payload) + self._attr_hvac_action = HVACAction(str(payload)) except ValueError: _LOGGER.warning( "Invalid %s action: %s", @@ -542,7 +560,9 @@ def handle_action_received(msg): add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) @callback - def handle_temperature_received(msg, template_name, attr): + def handle_temperature_received( + msg: ReceiveMessage, template_name: str, attr: str + ) -> None: """Handle temperature coming via MQTT.""" payload = render_template(msg, template_name) @@ -554,7 +574,7 @@ def handle_temperature_received(msg, template_name, attr): @callback @log_messages(self.hass, self.entity_id) - def handle_current_temperature_received(msg): + def handle_current_temperature_received(msg: ReceiveMessage) -> None: """Handle current temperature coming via MQTT.""" handle_temperature_received( msg, CONF_CURRENT_TEMP_TEMPLATE, "_attr_current_temperature" @@ -566,7 +586,7 @@ def handle_current_temperature_received(msg): @callback @log_messages(self.hass, self.entity_id) - def handle_target_temperature_received(msg): + def handle_target_temperature_received(msg: ReceiveMessage) -> None: """Handle target temperature coming via MQTT.""" handle_temperature_received( msg, CONF_TEMP_STATE_TEMPLATE, "_attr_target_temperature" @@ -578,7 +598,7 @@ def handle_target_temperature_received(msg): @callback @log_messages(self.hass, self.entity_id) - def handle_temperature_low_received(msg): + def handle_temperature_low_received(msg: ReceiveMessage) -> None: """Handle target temperature low coming via MQTT.""" handle_temperature_received( msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_attr_target_temperature_low" @@ -590,7 +610,7 @@ def handle_temperature_low_received(msg): @callback @log_messages(self.hass, self.entity_id) - def handle_temperature_high_received(msg): + def handle_temperature_high_received(msg: ReceiveMessage) -> None: """Handle target temperature high coming via MQTT.""" handle_temperature_received( msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_attr_target_temperature_high" @@ -601,7 +621,9 @@ def handle_temperature_high_received(msg): ) @callback - def handle_mode_received(msg, template_name, attr, mode_list): + def handle_mode_received( + msg: ReceiveMessage, template_name: str, attr: str, mode_list: str + ) -> None: """Handle receiving listed mode via MQTT.""" payload = render_template(msg, template_name) @@ -613,7 +635,7 @@ def handle_mode_received(msg, template_name, attr, mode_list): @callback @log_messages(self.hass, self.entity_id) - def handle_current_mode_received(msg): + def handle_current_mode_received(msg: ReceiveMessage) -> None: """Handle receiving mode via MQTT.""" handle_mode_received( msg, CONF_MODE_STATE_TEMPLATE, "_attr_hvac_mode", CONF_MODE_LIST @@ -623,7 +645,7 @@ def handle_current_mode_received(msg): @callback @log_messages(self.hass, self.entity_id) - def handle_fan_mode_received(msg): + def handle_fan_mode_received(msg: ReceiveMessage) -> None: """Handle receiving fan mode via MQTT.""" handle_mode_received( msg, @@ -636,7 +658,7 @@ def handle_fan_mode_received(msg): @callback @log_messages(self.hass, self.entity_id) - def handle_swing_mode_received(msg): + def handle_swing_mode_received(msg: ReceiveMessage) -> None: """Handle receiving swing mode via MQTT.""" handle_mode_received( msg, @@ -650,11 +672,13 @@ def handle_swing_mode_received(msg): ) @callback - def handle_onoff_mode_received(msg, template_name, attr): + def handle_onoff_mode_received( + msg: ReceiveMessage, template_name: str, attr: str + ) -> None: """Handle receiving on/off mode via MQTT.""" payload = render_template(msg, template_name) - payload_on = self._config[CONF_PAYLOAD_ON] - payload_off = self._config[CONF_PAYLOAD_OFF] + payload_on: str = self._config[CONF_PAYLOAD_ON] + payload_off: str = self._config[CONF_PAYLOAD_OFF] if payload == "True": payload = payload_on @@ -672,7 +696,7 @@ def handle_onoff_mode_received(msg, template_name, attr): @callback @log_messages(self.hass, self.entity_id) - def handle_aux_mode_received(msg): + def handle_aux_mode_received(msg: ReceiveMessage) -> None: """Handle receiving aux mode via MQTT.""" handle_onoff_mode_received( msg, CONF_AUX_STATE_TEMPLATE, "_attr_is_aux_heat" @@ -682,7 +706,7 @@ def handle_aux_mode_received(msg): @callback @log_messages(self.hass, self.entity_id) - def handle_preset_mode_received(msg): + def handle_preset_mode_received(msg: ReceiveMessage) -> None: """Handle receiving preset mode via MQTT.""" preset_mode = render_template(msg, CONF_PRESET_MODE_VALUE_TEMPLATE) if preset_mode in [PRESET_NONE, PAYLOAD_NONE]: @@ -692,7 +716,7 @@ def handle_preset_mode_received(msg): if not preset_mode: _LOGGER.debug("Ignoring empty preset_mode from '%s'", msg.topic) return - if preset_mode not in self.preset_modes: + if not self.preset_modes or preset_mode not in self.preset_modes: _LOGGER.warning( "'%s' received on topic %s. '%s' is not a valid preset mode", msg.payload, @@ -700,7 +724,7 @@ def handle_preset_mode_received(msg): preset_mode, ) else: - self._attr_preset_mode = preset_mode + self._attr_preset_mode = str(preset_mode) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -712,11 +736,11 @@ def handle_preset_mode_received(msg): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - async def _publish(self, topic, payload): + async def _publish(self, topic: str, payload: PublishPayloadType) -> None: if self._topic[topic] is not None: await self.async_publish( self._topic[topic], @@ -727,8 +751,13 @@ async def _publish(self, topic, payload): ) async def _set_temperature( - self, temp, cmnd_topic, cmnd_template, state_topic, attr - ): + self, + temp: float | None, + cmnd_topic: str, + cmnd_template: str, + state_topic: str, + attr: str, + ) -> None: if temp is not None: if self._topic[state_topic] is None: # optimistic mode @@ -822,7 +851,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: return - async def _set_aux_heat(self, state): + async def _set_aux_heat(self, state: bool) -> None: await self._publish( CONF_AUX_COMMAND_TOPIC, self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], From 8c0a7b9d7fb98084e72c3fc00bab9704019f90f2 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 09:24:16 +0100 Subject: [PATCH 298/394] Add type hints for MQTT tag (#81495) * Improve typing tag * Additional type hints for tag platform * Follow up comment tag * Folow up comments * Revert comment removal --- homeassistant/components/mqtt/tag.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 23afae35cc9ad5..26cb66e9f2fddd 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -1,6 +1,7 @@ """Provides tag scanning for MQTT.""" from __future__ import annotations +from collections.abc import Callable import functools import voluptuous as vol @@ -9,11 +10,12 @@ from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import MQTT_BASE_SCHEMA from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC +from .discovery import MQTTDiscoveryPayload from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, MqttDiscoveryDeviceUpdate, @@ -21,7 +23,7 @@ send_discovery_done, update_device, ) -from .models import MqttValueTemplate, ReceiveMessage +from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType from .subscription import EntitySubscription from .util import get_mqtt_data, valid_subscribe_topic @@ -87,12 +89,14 @@ def async_has_tags(hass: HomeAssistant, device_id: str) -> bool: class MQTTTagScanner(MqttDiscoveryDeviceUpdate): """MQTT Tag scanner.""" + _value_template: Callable[[ReceivePayloadType, str], ReceivePayloadType] + def __init__( self, hass: HomeAssistant, config: ConfigType, device_id: str | None, - discovery_data: dict, + discovery_data: DiscoveryInfoType, config_entry: ConfigEntry, ) -> None: """Initialize.""" @@ -111,10 +115,10 @@ def __init__( self, hass, discovery_data, device_id, config_entry, LOG_NAME ) - async def async_update(self, discovery_data: dict) -> None: + async def async_update(self, discovery_data: MQTTDiscoveryPayload) -> None: """Handle MQTT tag discovery updates.""" # Update tag scanner - config = PLATFORM_SCHEMA(discovery_data) + config: DiscoveryInfoType = PLATFORM_SCHEMA(discovery_data) self._config = config self._value_template = MqttValueTemplate( config.get(CONF_VALUE_TEMPLATE), @@ -127,7 +131,7 @@ async def subscribe_topics(self) -> None: """Subscribe to MQTT topics.""" async def tag_scanned(msg: ReceiveMessage) -> None: - tag_id = self._value_template(msg.payload, "").strip() + tag_id = str(self._value_template(msg.payload, "")).strip() if not tag_id: # No output from template, ignore return From d66d079330b92c02c38fb1c9dca539617161fdbc Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 10:16:05 +0100 Subject: [PATCH 299/394] Use `_attr_` for MQTT light (#81465) * Schema basic * Schema json * Schema template * add color_mode - follow up comments * Fix regression * Follow up comments 2 * Fix mypy errors * Update homeassistant/components/mqtt/light/schema_template.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/mqtt/light/schema_basic.py | 202 +++++---------- .../components/mqtt/light/schema_json.py | 236 +++++++----------- .../components/mqtt/light/schema_template.py | 144 ++++------- 3 files changed, 195 insertions(+), 387 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index bf2ae33ca1d457..19d059ae8cccc9 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -1,5 +1,6 @@ """Support for MQTT lights.""" import logging +from typing import cast import voluptuous as vol @@ -256,18 +257,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" - self._brightness = None - self._color_mode = None - self._color_temp = None - self._effect = None - self._hs_color = None - self._rgb_color = None - self._rgbw_color = None - self._rgbww_color = None - self._state = None - self._supported_color_modes = None - self._xy_color = None - self._topic = None self._payload = None self._command_templates = None @@ -292,6 +281,10 @@ def config_schema(): def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._attr_min_mireds = config.get(CONF_MIN_MIREDS, super().min_mireds) + self._attr_max_mireds = config.get(CONF_MAX_MIREDS, super().max_mireds) + self._attr_effect_list = config.get(CONF_EFFECT_LIST) + if CONF_STATE_VALUE_TEMPLATE not in config and CONF_VALUE_TEMPLATE in config: config[CONF_STATE_VALUE_TEMPLATE] = config[CONF_VALUE_TEMPLATE] @@ -378,39 +371,47 @@ def _setup_from_config(self, config): supported_color_modes = set() if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.COLOR_TEMP) - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP if topic[CONF_HS_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.HS) - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS if topic[CONF_RGB_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.RGB) - self._color_mode = ColorMode.RGB + self._attr_color_mode = ColorMode.RGB if topic[CONF_RGBW_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.RGBW) - self._color_mode = ColorMode.RGBW + self._attr_color_mode = ColorMode.RGBW if topic[CONF_RGBWW_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.RGBWW) - self._color_mode = ColorMode.RGBWW + self._attr_color_mode = ColorMode.RGBWW if topic[CONF_WHITE_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.WHITE) if topic[CONF_XY_COMMAND_TOPIC] is not None: supported_color_modes.add(ColorMode.XY) - self._color_mode = ColorMode.XY + self._attr_color_mode = ColorMode.XY if len(supported_color_modes) > 1: - self._color_mode = ColorMode.UNKNOWN + self._attr_color_mode = ColorMode.UNKNOWN if not supported_color_modes: if topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: - self._color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = ColorMode.BRIGHTNESS supported_color_modes.add(ColorMode.BRIGHTNESS) else: - self._color_mode = ColorMode.ONOFF + self._attr_color_mode = ColorMode.ONOFF supported_color_modes.add(ColorMode.ONOFF) # Validate the color_modes configuration - self._supported_color_modes = valid_supported_color_modes(supported_color_modes) + self._attr_supported_color_modes = valid_supported_color_modes( + supported_color_modes + ) + + supported_features: int = 0 + supported_features |= ( + topic[CONF_EFFECT_COMMAND_TOPIC] is not None and LightEntityFeature.EFFECT + ) + self._attr_supported_features = supported_features - def _is_optimistic(self, attribute): + def _is_optimistic(self, attribute: str) -> bool: """Return True if the attribute is optimistically updated.""" return getattr(self, f"_optimistic_{attribute}") @@ -438,11 +439,11 @@ def state_received(msg): return if payload == self._payload["on"]: - self._state = True + self._attr_is_on = True elif payload == self._payload["off"]: - self._state = False + self._attr_is_on = False elif payload == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._topic[CONF_STATE_TOPIC] is not None: @@ -466,7 +467,8 @@ def brightness_received(msg: ReceiveMessage) -> None: device_value = float(payload) percent_bright = device_value / self._config[CONF_BRIGHTNESS_SCALE] - self._brightness = percent_bright * 255 + self._attr_brightness = min(round(percent_bright * 255), 255) + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_BRIGHTNESS_STATE_TOPIC, brightness_received) @@ -483,11 +485,11 @@ def _rgbx_received(msg, template, color_mode, convert_color): return None color = tuple(int(val) for val in payload.split(",")) if self._optimistic_color_mode: - self._color_mode = color_mode + self._attr_color_mode = color_mode if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is None: rgb = convert_color(*color) percent_bright = float(color_util.color_RGB_to_hsv(*rgb)[2]) / 100.0 - self._brightness = percent_bright * 255 + self._attr_brightness = min(round(percent_bright * 255), 255) return color @callback @@ -499,7 +501,7 @@ def rgb_received(msg): ) if not rgb: return - self._rgb_color = rgb + self._attr_rgb_color = rgb get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -516,7 +518,7 @@ def rgbw_received(msg): ) if not rgbw: return - self._rgbw_color = rgbw + self._attr_rgbw_color = rgbw get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBW_STATE_TOPIC, rgbw_received) @@ -533,7 +535,7 @@ def rgbww_received(msg): ) if not rgbww: return - self._rgbww_color = rgbww + self._attr_rgbww_color = rgbww get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_RGBWW_STATE_TOPIC, rgbww_received) @@ -549,7 +551,7 @@ def color_mode_received(msg: ReceiveMessage) -> None: _LOGGER.debug("Ignoring empty color mode message from '%s'", msg.topic) return - self._color_mode = payload + self._attr_color_mode = payload get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_MODE_STATE_TOPIC, color_mode_received) @@ -566,8 +568,8 @@ def color_temp_received(msg: ReceiveMessage) -> None: return if self._optimistic_color_mode: - self._color_mode = ColorMode.COLOR_TEMP - self._color_temp = int(payload) + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = int(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_COLOR_TEMP_STATE_TOPIC, color_temp_received) @@ -583,7 +585,7 @@ def effect_received(msg: ReceiveMessage) -> None: _LOGGER.debug("Ignoring empty effect message from '%s'", msg.topic) return - self._effect = payload + self._attr_effect = payload get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_EFFECT_STATE_TOPIC, effect_received) @@ -599,10 +601,13 @@ def hs_received(msg: ReceiveMessage) -> None: _LOGGER.debug("Ignoring empty hs message from '%s'", msg.topic) return try: - hs_color = tuple(float(val) for val in payload.split(",", 2)) + hs_color = cast( + tuple[float, float], + tuple(float(val) for val in payload.split(",", 2)), + ) if self._optimistic_color_mode: - self._color_mode = ColorMode.HS - self._hs_color = hs_color + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: _LOGGER.debug("Failed to parse hs state update: '%s'", payload) @@ -620,10 +625,12 @@ def xy_received(msg: ReceiveMessage) -> None: _LOGGER.debug("Ignoring empty xy-color message from '%s'", msg.topic) return - xy_color = tuple(float(val) for val in payload.split(",")) + xy_color = cast( + tuple[float, float], tuple(float(val) for val in payload.split(",", 2)) + ) if self._optimistic_color_mode: - self._color_mode = ColorMode.XY - self._xy_color = xy_color + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy_color get_mqtt_data(self.hass).state_write_requests.write_state_request(self) add_topic(CONF_XY_STATE_TOPIC, xy_received) @@ -643,10 +650,10 @@ def restore_state(attribute, condition_attribute=None): condition_attribute = attribute optimistic = self._is_optimistic(condition_attribute) if optimistic and last_state and last_state.attributes.get(attribute): - setattr(self, f"_{attribute}", last_state.attributes[attribute]) + setattr(self, f"_attr_{attribute}", last_state.attributes[attribute]) if self._topic[CONF_STATE_TOPIC] is None and self._optimistic and last_state: - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON restore_state(ATTR_BRIGHTNESS) restore_state(ATTR_RGB_COLOR) restore_state(ATTR_HS_COLOR, ATTR_RGB_COLOR) @@ -659,93 +666,11 @@ def restore_state(attribute, condition_attribute=None): restore_state(ATTR_XY_COLOR) restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - if brightness := self._brightness: - brightness = min(round(brightness), 255) - return brightness - - @property - def color_mode(self): - """Return current color mode.""" - return self._color_mode - - @property - def hs_color(self): - """Return the hs color value.""" - return self._hs_color - - @property - def rgb_color(self): - """Return the rgb color value.""" - return self._rgb_color - - @property - def rgbw_color(self): - """Return the rgbw color value.""" - return self._rgbw_color - - @property - def rgbww_color(self): - """Return the rgbww color value.""" - return self._rgbww_color - - @property - def xy_color(self): - """Return the xy color value.""" - return self._xy_color - - @property - def color_temp(self): - """Return the color temperature in mired.""" - return self._color_temp - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._config.get(CONF_MIN_MIREDS, super().min_mireds) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - @property def assumed_state(self): """Return true if we do optimistic updates.""" return self._optimistic - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._config.get(CONF_EFFECT_LIST) - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - supported_features = 0 - supported_features |= ( - self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None - and LightEntityFeature.EFFECT - ) - return supported_features - async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. @@ -772,9 +697,7 @@ def scale_rgbx(color, brightness=None): if self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: brightness = 255 else: - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 - ) + brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) return tuple(int(channel * brightness / 255) for channel in color) def render_rgbx(color, template, color_mode): @@ -797,8 +720,9 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): if not self._is_optimistic(condition_attribute): return False if color_mode and self._optimistic_color_mode: - self._color_mode = color_mode - setattr(self, f"_{attribute}", value) + self._attr_color_mode = color_mode + + setattr(self, f"_attr_{attribute}", value) return True if on_command_type == "first": @@ -813,7 +737,7 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): and ATTR_BRIGHTNESS not in kwargs and ATTR_WHITE not in kwargs ): - kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 + kwargs[ATTR_BRIGHTNESS] = self.brightness or 255 hs_color = kwargs.get(ATTR_HS_COLOR) @@ -871,7 +795,7 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): and ATTR_RGB_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None ): - rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 + rgb_color = self.rgb_color or (255,) * 3 rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) @@ -881,9 +805,7 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): and ATTR_RGBW_COLOR not in kwargs and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None ): - rgbw_color = ( - self._rgbw_color if self._rgbw_color is not None else (255,) * 4 - ) + rgbw_color = self.rgbw_color or (255,) * 4 rgbw = scale_rgbx(rgbw_color, kwargs[ATTR_BRIGHTNESS]) rgbw_s = render_rgbx(rgbw, CONF_RGBW_COMMAND_TEMPLATE, ColorMode.RGBW) await publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) @@ -893,9 +815,7 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): and ATTR_RGBWW_COLOR not in kwargs and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None ): - rgbww_color = ( - self._rgbww_color if self._rgbww_color is not None else (255,) * 5 - ) + rgbww_color = self.rgbww_color or (255,) * 5 rgbww = scale_rgbx(rgbww_color, kwargs[ATTR_BRIGHTNESS]) rgbww_s = render_rgbx(rgbww, CONF_RGBWW_COMMAND_TEMPLATE, ColorMode.RGBWW) await publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) @@ -938,7 +858,7 @@ def set_optimistic(attribute, value, color_mode=None, condition_attribute=None): if self._optimistic: # Optimistically assume that the light has changed state. - self._state = True + self._attr_is_on = True should_update = True if should_update: @@ -959,5 +879,5 @@ async def async_turn_off(self, **kwargs): if self._optimistic: # Optimistically assume that the light has changed state. - self._state = False + self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index a4a76673176dae..b5824e5e456d66 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -188,22 +188,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT JSON light.""" - self._state = None - self._supported_features = 0 - self._topic = None self._optimistic = False - self._brightness = None - self._color_mode = None - self._color_temp = None - self._effect = None self._fixed_color_mode = None self._flash_times = None - self._hs = None - self._rgb = None - self._rgbw = None - self._rgbww = None - self._xy = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -214,6 +202,10 @@ def config_schema(): def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._attr_max_mireds = config.get(CONF_MAX_MIREDS, super().max_mireds) + self._attr_min_mireds = config.get(CONF_MIN_MIREDS, super().min_mireds) + self._attr_effect_list = config.get(CONF_EFFECT_LIST) + self._topic = { key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC) } @@ -225,10 +217,12 @@ def _setup_from_config(self, config): for key in (CONF_FLASH_TIME_SHORT, CONF_FLASH_TIME_LONG) } - self._supported_features = ( + self._attr_supported_features = ( LightEntityFeature.TRANSITION | LightEntityFeature.FLASH ) - self._supported_features |= config[CONF_EFFECT] and LightEntityFeature.EFFECT + self._attr_supported_features |= ( + config[CONF_EFFECT] and LightEntityFeature.EFFECT + ) if not self._config[CONF_COLOR_MODE]: color_modes = {ColorMode.ONOFF} if config[CONF_BRIGHTNESS]: @@ -237,13 +231,13 @@ def _setup_from_config(self, config): color_modes.add(ColorMode.COLOR_TEMP) if config[CONF_HS] or config[CONF_RGB] or config[CONF_XY]: color_modes.add(ColorMode.HS) - self._supported_color_modes = filter_supported_color_modes(color_modes) - if len(self._supported_color_modes) == 1: - self._fixed_color_mode = next(iter(self._supported_color_modes)) + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) + if len(self.supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self.supported_color_modes)) else: - self._supported_color_modes = self._config[CONF_SUPPORTED_COLOR_MODES] - if len(self._supported_color_modes) == 1: - self._color_mode = next(iter(self._supported_color_modes)) + self._attr_supported_color_modes = self._config[CONF_SUPPORTED_COLOR_MODES] + if len(self.supported_color_modes) == 1: + self._attr_color_mode = next(iter(self.supported_color_modes)) def _update_color(self, values): if not self._config[CONF_COLOR_MODE]: @@ -252,7 +246,7 @@ def _update_color(self, values): red = int(values["color"]["r"]) green = int(values["color"]["g"]) blue = int(values["color"]["b"]) - self._hs = color_util.color_RGB_to_hs(red, green, blue) + self._attr_hs_color = color_util.color_RGB_to_hs(red, green, blue) except KeyError: pass except ValueError: @@ -264,7 +258,7 @@ def _update_color(self, values): try: x_color = float(values["color"]["x"]) y_color = float(values["color"]["y"]) - self._hs = color_util.color_xy_to_hs(x_color, y_color) + self._attr_hs_color = color_util.color_xy_to_hs(x_color, y_color) except KeyError: pass except ValueError: @@ -276,7 +270,7 @@ def _update_color(self, values): try: hue = float(values["color"]["h"]) saturation = float(values["color"]["s"]) - self._hs = (hue, saturation) + self._attr_hs_color = (hue, saturation) except KeyError: pass except ValueError: @@ -293,41 +287,41 @@ def _update_color(self, values): return try: if color_mode == ColorMode.COLOR_TEMP: - self._color_temp = int(values["color_temp"]) - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = int(values["color_temp"]) + self._attr_color_mode = ColorMode.COLOR_TEMP elif color_mode == ColorMode.HS: hue = float(values["color"]["h"]) saturation = float(values["color"]["s"]) - self._color_mode = ColorMode.HS - self._hs = (hue, saturation) + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = (hue, saturation) elif color_mode == ColorMode.RGB: r = int(values["color"]["r"]) # pylint: disable=invalid-name g = int(values["color"]["g"]) # pylint: disable=invalid-name b = int(values["color"]["b"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.RGB - self._rgb = (r, g, b) + self._attr_color_mode = ColorMode.RGB + self._attr_rgb_color = (r, g, b) elif color_mode == ColorMode.RGBW: r = int(values["color"]["r"]) # pylint: disable=invalid-name g = int(values["color"]["g"]) # pylint: disable=invalid-name b = int(values["color"]["b"]) # pylint: disable=invalid-name w = int(values["color"]["w"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.RGBW - self._rgbw = (r, g, b, w) + self._attr_color_mode = ColorMode.RGBW + self._attr_rgbw_color = (r, g, b, w) elif color_mode == ColorMode.RGBWW: r = int(values["color"]["r"]) # pylint: disable=invalid-name g = int(values["color"]["g"]) # pylint: disable=invalid-name b = int(values["color"]["b"]) # pylint: disable=invalid-name c = int(values["color"]["c"]) # pylint: disable=invalid-name w = int(values["color"]["w"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.RGBWW - self._rgbww = (r, g, b, c, w) + self._attr_color_mode = ColorMode.RGBWW + self._attr_rgbww_color = (r, g, b, c, w) elif color_mode == ColorMode.WHITE: - self._color_mode = ColorMode.WHITE + self._attr_color_mode = ColorMode.WHITE elif color_mode == ColorMode.XY: x = float(values["color"]["x"]) # pylint: disable=invalid-name y = float(values["color"]["y"]) # pylint: disable=invalid-name - self._color_mode = ColorMode.XY - self._xy = (x, y) + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = (x, y) except (KeyError, ValueError): _LOGGER.warning( "Invalid or incomplete color value received for entity %s", @@ -344,29 +338,29 @@ def state_received(msg): values = json_loads(msg.payload) if values["state"] == "ON": - self._state = True + self._attr_is_on = True elif values["state"] == "OFF": - self._state = False + self._attr_is_on = False elif values["state"] is None: - self._state = None + self._attr_is_on = None if ( not self._config[CONF_COLOR_MODE] - and color_supported(self._supported_color_modes) + and color_supported(self.supported_color_modes) and "color" in values ): # Deprecated color handling if values["color"] is None: - self._hs = None + self._attr_hs_color = None else: self._update_color(values) if self._config[CONF_COLOR_MODE] and "color_mode" in values: self._update_color(values) - if brightness_supported(self._supported_color_modes): + if brightness_supported(self.supported_color_modes): try: - self._brightness = int( + self._attr_brightness = int( values["brightness"] / float(self._config[CONF_BRIGHTNESS_SCALE]) * 255 @@ -380,15 +374,15 @@ def state_received(msg): ) if ( - ColorMode.COLOR_TEMP in self._supported_color_modes + ColorMode.COLOR_TEMP in self.supported_color_modes and not self._config[CONF_COLOR_MODE] ): # Deprecated color handling try: if values["color_temp"] is None: - self._color_temp = None + self._attr_color_temp = None else: - self._color_temp = int(values["color_temp"]) + self._attr_color_temp = int(values["color_temp"]) except KeyError: pass except ValueError: @@ -397,9 +391,9 @@ def state_received(msg): self.entity_id, ) - if self._supported_features and LightEntityFeature.EFFECT: + if self.supported_features and LightEntityFeature.EFFECT: with suppress(KeyError): - self._effect = values["effect"] + self._attr_effect = values["effect"] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -423,77 +417,27 @@ async def _subscribe_topics(self): last_state = await self.async_get_last_state() if self._optimistic and last_state: - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON last_attributes = last_state.attributes - self._brightness = last_attributes.get(ATTR_BRIGHTNESS, self._brightness) - self._color_mode = last_attributes.get(ATTR_COLOR_MODE, self._color_mode) - self._color_temp = last_attributes.get(ATTR_COLOR_TEMP, self._color_temp) - self._effect = last_attributes.get(ATTR_EFFECT, self._effect) - self._hs = last_attributes.get(ATTR_HS_COLOR, self._hs) - self._rgb = last_attributes.get(ATTR_RGB_COLOR, self._rgb) - self._rgbw = last_attributes.get(ATTR_RGBW_COLOR, self._rgbw) - self._rgbww = last_attributes.get(ATTR_RGBWW_COLOR, self._rgbww) - self._xy = last_attributes.get(ATTR_XY_COLOR, self._xy) - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def color_temp(self): - """Return the color temperature in mired.""" - return self._color_temp - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._config.get(CONF_MIN_MIREDS, super().min_mireds) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._config.get(CONF_EFFECT_LIST) - - @property - def hs_color(self): - """Return the hs color value.""" - return self._hs - - @property - def rgb_color(self): - """Return the hs color value.""" - return self._rgb - - @property - def rgbw_color(self): - """Return the hs color value.""" - return self._rgbw - - @property - def rgbww_color(self): - """Return the hs color value.""" - return self._rgbww - - @property - def xy_color(self): - """Return the hs color value.""" - return self._xy - - @property - def is_on(self): - """Return true if device is on.""" - return self._state + self._attr_brightness = last_attributes.get( + ATTR_BRIGHTNESS, self.brightness + ) + self._attr_color_mode = last_attributes.get( + ATTR_COLOR_MODE, self.color_mode + ) + self._attr_color_temp = last_attributes.get( + ATTR_COLOR_TEMP, self.color_temp + ) + self._attr_effect = last_attributes.get(ATTR_EFFECT, self.effect) + self._attr_hs_color = last_attributes.get(ATTR_HS_COLOR, self.hs_color) + self._attr_rgb_color = last_attributes.get(ATTR_RGB_COLOR, self.rgb_color) + self._attr_rgbw_color = last_attributes.get( + ATTR_RGBW_COLOR, self.rgbw_color + ) + self._attr_rgbww_color = last_attributes.get( + ATTR_RGBWW_COLOR, self.rgbww_color + ) + self._attr_xy_color = last_attributes.get(ATTR_XY_COLOR, self.xy_color) @property def assumed_state(self): @@ -504,25 +448,15 @@ def assumed_state(self): def color_mode(self): """Return current color mode.""" if self._config[CONF_COLOR_MODE]: - return self._color_mode + return self._attr_color_mode if self._fixed_color_mode: # Legacy light with support for a single color mode return self._fixed_color_mode # Legacy light with support for ct + hs, prioritize hs - if self._hs is not None: + if self.hs_color is not None: return ColorMode.HS return ColorMode.COLOR_TEMP - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - def _set_flash_and_transition(self, message, **kwargs): if ATTR_TRANSITION in kwargs: message["transition"] = kwargs[ATTR_TRANSITION] @@ -587,32 +521,32 @@ async def async_turn_on(self, **kwargs): # noqa: C901 message["color"]["s"] = hs_color[1] if self._optimistic: - self._color_temp = None - self._hs = kwargs[ATTR_HS_COLOR] + self._attr_color_temp = None + self._attr_hs_color = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_HS_COLOR in kwargs and self._supports_color_mode(ColorMode.HS): hs_color = kwargs[ATTR_HS_COLOR] message["color"] = {"h": hs_color[0], "s": hs_color[1]} if self._optimistic: - self._color_mode = ColorMode.HS - self._hs = hs_color + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color should_update = True if ATTR_RGB_COLOR in kwargs and self._supports_color_mode(ColorMode.RGB): rgb = self._scale_rgbxx(kwargs[ATTR_RGB_COLOR], kwargs) message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2]} if self._optimistic: - self._color_mode = ColorMode.RGB - self._rgb = rgb + self._attr_color_mode = ColorMode.RGB + self._attr_rgb_color = rgb should_update = True if ATTR_RGBW_COLOR in kwargs and self._supports_color_mode(ColorMode.RGBW): rgb = self._scale_rgbxx(kwargs[ATTR_RGBW_COLOR], kwargs) message["color"] = {"r": rgb[0], "g": rgb[1], "b": rgb[2], "w": rgb[3]} if self._optimistic: - self._color_mode = ColorMode.RGBW - self._rgbw = rgb + self._attr_color_mode = ColorMode.RGBW + self._attr_rgbw_color = rgb should_update = True if ATTR_RGBWW_COLOR in kwargs and self._supports_color_mode(ColorMode.RGBWW): @@ -625,16 +559,16 @@ async def async_turn_on(self, **kwargs): # noqa: C901 "w": rgb[4], } if self._optimistic: - self._color_mode = ColorMode.RGBWW - self._rgbww = rgb + self._attr_color_mode = ColorMode.RGBWW + self._attr_rgbww_color = rgb should_update = True if ATTR_XY_COLOR in kwargs and self._supports_color_mode(ColorMode.XY): xy = kwargs[ATTR_XY_COLOR] # pylint: disable=invalid-name message["color"] = {"x": xy[0], "y": xy[1]} if self._optimistic: - self._color_mode = ColorMode.XY - self._xy = xy + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy should_update = True self._set_flash_and_transition(message, **kwargs) @@ -650,23 +584,23 @@ async def async_turn_on(self, **kwargs): # noqa: C901 message["brightness"] = device_brightness if self._optimistic: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] should_update = True if ATTR_COLOR_TEMP in kwargs: message["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if self._optimistic: - self._color_mode = ColorMode.COLOR_TEMP - self._color_temp = kwargs[ATTR_COLOR_TEMP] - self._hs = None + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = kwargs[ATTR_COLOR_TEMP] + self._attr_hs_color = None should_update = True if ATTR_EFFECT in kwargs: message["effect"] = kwargs[ATTR_EFFECT] if self._optimistic: - self._effect = kwargs[ATTR_EFFECT] + self._attr_effect = kwargs[ATTR_EFFECT] should_update = True if ATTR_WHITE in kwargs and self._supports_color_mode(ColorMode.WHITE): @@ -678,8 +612,8 @@ async def async_turn_on(self, **kwargs): # noqa: C901 message["white"] = device_white_level if self._optimistic: - self._color_mode = ColorMode.WHITE - self._brightness = kwargs[ATTR_WHITE] + self._attr_color_mode = ColorMode.WHITE + self._attr_brightness = kwargs[ATTR_WHITE] should_update = True await self.async_publish( @@ -692,7 +626,7 @@ async def async_turn_on(self, **kwargs): # noqa: C901 if self._optimistic: # Optimistically assume that the light has changed state. - self._state = True + self._attr_is_on = True should_update = True if should_update: @@ -717,5 +651,5 @@ async def async_turn_off(self, **kwargs): if self._optimistic: # Optimistically assume that the light has changed state. - self._state = False + self._attr_is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 33c7f1cea1bba8..b57e09e0c1f818 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -121,18 +121,12 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize a MQTT Template light.""" - self._state = None - self._topics = None self._templates = None self._optimistic = False # features - self._brightness = None self._fixed_color_mode = None - self._color_temp = None - self._hs = None - self._effect = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -143,6 +137,10 @@ def config_schema(): def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._attr_max_mireds = config.get(CONF_MAX_MIREDS, super().max_mireds) + self._attr_min_mireds = config.get(CONF_MIN_MIREDS, super().min_mireds) + self._attr_effect_list = config.get(CONF_EFFECT_LIST) + self._topics = { key: config.get(key) for key in (CONF_STATE_TOPIC, CONF_COMMAND_TOPIC) } @@ -178,9 +176,23 @@ def _setup_from_config(self, config): and self._templates[CONF_BLUE_TEMPLATE] is not None ): color_modes.add(ColorMode.HS) - self._supported_color_modes = filter_supported_color_modes(color_modes) - if len(self._supported_color_modes) == 1: - self._fixed_color_mode = next(iter(self._supported_color_modes)) + self._attr_supported_color_modes = filter_supported_color_modes(color_modes) + self._fixed_color_mode = None + if len(self.supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self.supported_color_modes)) + self._attr_color_mode = self._fixed_color_mode + + features = LightEntityFeature.FLASH | LightEntityFeature.TRANSITION + if config.get(CONF_EFFECT_LIST) is not None: + features = features | LightEntityFeature.EFFECT + self._attr_supported_features = features + + def _update_color_mode(self): + """Update the color_mode attribute.""" + if self._fixed_color_mode: + return + # Support for ct + hs, prioritize hs + self._attr_color_mode = ColorMode.HS if self.hs_color else ColorMode.COLOR_TEMP def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -196,17 +208,17 @@ def state_received(msg): CONF_STATE_TEMPLATE ].async_render_with_possible_json_value(msg.payload) if state == STATE_ON: - self._state = True + self._attr_is_on = True elif state == STATE_OFF: - self._state = False + self._attr_is_on = False elif state == PAYLOAD_NONE: - self._state = None + self._attr_is_on = None else: _LOGGER.warning("Invalid state value received") if self._templates[CONF_BRIGHTNESS_TEMPLATE] is not None: try: - self._brightness = int( + self._attr_brightness = int( self._templates[ CONF_BRIGHTNESS_TEMPLATE ].async_render_with_possible_json_value(msg.payload) @@ -219,7 +231,9 @@ def state_received(msg): color_temp = self._templates[ CONF_COLOR_TEMP_TEMPLATE ].async_render_with_possible_json_value(msg.payload) - self._color_temp = int(color_temp) if color_temp != "None" else None + self._attr_color_temp = ( + int(color_temp) if color_temp != "None" else None + ) except ValueError: _LOGGER.warning("Invalid color temperature value received") @@ -239,11 +253,12 @@ def state_received(msg): CONF_BLUE_TEMPLATE ].async_render_with_possible_json_value(msg.payload) if red == "None" and green == "None" and blue == "None": - self._hs = None + self._attr_hs_color = None else: - self._hs = color_util.color_RGB_to_hs( + self._attr_hs_color = color_util.color_RGB_to_hs( int(red), int(green), int(blue) ) + self._update_color_mode() except ValueError: _LOGGER.warning("Invalid color value received") @@ -253,7 +268,7 @@ def state_received(msg): ].async_render_with_possible_json_value(msg.payload) if effect in self._config.get(CONF_EFFECT_LIST): - self._effect = effect + self._attr_effect = effect else: _LOGGER.warning("Unsupported effect value received") @@ -279,61 +294,22 @@ async def _subscribe_topics(self): last_state = await self.async_get_last_state() if self._optimistic and last_state: - self._state = last_state.state == STATE_ON + self._attr_is_on = last_state.state == STATE_ON if last_state.attributes.get(ATTR_BRIGHTNESS): - self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + self._attr_brightness = last_state.attributes.get(ATTR_BRIGHTNESS) if last_state.attributes.get(ATTR_HS_COLOR): - self._hs = last_state.attributes.get(ATTR_HS_COLOR) + self._attr_hs_color = last_state.attributes.get(ATTR_HS_COLOR) + self._update_color_mode() if last_state.attributes.get(ATTR_COLOR_TEMP): - self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + self._attr_color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) if last_state.attributes.get(ATTR_EFFECT): - self._effect = last_state.attributes.get(ATTR_EFFECT) - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def color_temp(self): - """Return the color temperature in mired.""" - return self._color_temp - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._config.get(CONF_MIN_MIREDS, super().min_mireds) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - - @property - def hs_color(self): - """Return the hs color value [int, int].""" - return self._hs - - @property - def is_on(self): - """Return True if entity is on.""" - return self._state + self._attr_effect = last_state.attributes.get(ATTR_EFFECT) @property def assumed_state(self): """Return True if unable to access real state of the entity.""" return self._optimistic - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._config.get(CONF_EFFECT_LIST) - - @property - def effect(self): - """Return the current effect.""" - return self._effect - async def async_turn_on(self, **kwargs): """Turn the entity on. @@ -341,20 +317,21 @@ async def async_turn_on(self, **kwargs): """ values = {"state": True} if self._optimistic: - self._state = True + self._attr_is_on = True if ATTR_BRIGHTNESS in kwargs: values["brightness"] = int(kwargs[ATTR_BRIGHTNESS]) if self._optimistic: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] if ATTR_COLOR_TEMP in kwargs: values["color_temp"] = int(kwargs[ATTR_COLOR_TEMP]) if self._optimistic: - self._color_temp = kwargs[ATTR_COLOR_TEMP] - self._hs = None + self._attr_color_temp = kwargs[ATTR_COLOR_TEMP] + self._attr_hs_color = None + self._update_color_mode() if ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] @@ -366,7 +343,7 @@ async def async_turn_on(self, **kwargs): else: brightness = kwargs.get( ATTR_BRIGHTNESS, - self._brightness if self._brightness is not None else 255, + self._attr_brightness if self._attr_brightness is not None else 255, ) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100 @@ -378,14 +355,15 @@ async def async_turn_on(self, **kwargs): values["sat"] = hs_color[1] if self._optimistic: - self._color_temp = None - self._hs = kwargs[ATTR_HS_COLOR] + self._attr_color_temp = None + self._attr_hs_color = kwargs[ATTR_HS_COLOR] + self._update_color_mode() if ATTR_EFFECT in kwargs: values["effect"] = kwargs.get(ATTR_EFFECT) if self._optimistic: - self._effect = kwargs[ATTR_EFFECT] + self._attr_effect = kwargs[ATTR_EFFECT] if ATTR_FLASH in kwargs: values["flash"] = kwargs.get(ATTR_FLASH) @@ -413,7 +391,7 @@ async def async_turn_off(self, **kwargs): """ values = {"state": False} if self._optimistic: - self._state = False + self._attr_is_on = False if ATTR_TRANSITION in kwargs: values["transition"] = kwargs[ATTR_TRANSITION] @@ -430,27 +408,3 @@ async def async_turn_off(self, **kwargs): if self._optimistic: self.async_write_ha_state() - - @property - def color_mode(self): - """Return current color mode.""" - if self._fixed_color_mode: - return self._fixed_color_mode - # Support for ct + hs, prioritize hs - if self._hs is not None: - return ColorMode.HS - return ColorMode.COLOR_TEMP - - @property - def supported_color_modes(self): - """Flag supported color modes.""" - return self._supported_color_modes - - @property - def supported_features(self): - """Flag supported features.""" - features = LightEntityFeature.FLASH | LightEntityFeature.TRANSITION - if self._config.get(CONF_EFFECT_LIST) is not None: - features = features | LightEntityFeature.EFFECT - - return features From 318122fe531ae021480da29e5d26e1b9cf1213bd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 10:38:29 +0100 Subject: [PATCH 300/394] Fix coordinator TypeVar definition (#81298) * Adjust coordinator TypeVar definition * Adjust again --- homeassistant/helpers/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 768b8040729ecc..d0d1cb904549c5 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -25,7 +25,7 @@ _T = TypeVar("_T") _DataUpdateCoordinatorT = TypeVar( - "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" + "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator" ) From fc0e0bf099c082a12b11578696a3b016cdc7a93f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 8 Nov 2022 10:49:47 +0100 Subject: [PATCH 301/394] Support for direct modbus connection to nibe pumps (#80557) * Initial support for modbus * Complete test coverage of config flow * Bump to 1.1.0 with corrected numbers * Handle missing mapping for reset-alarm * Fixup type checks after library bump * Adjust coil number * Move word_swap to nibegw * Adjust to 1.1.2 with fixes * Add series property * Add S series models * Correct test for invalid host * Apply suggestions from code review Co-authored-by: J. Nick Koston * Move some things into library * Adjust strings somewhat * Correct black * Correct test after validation change Co-authored-by: J. Nick Koston --- .../components/nibe_heatpump/__init__.py | 72 +++--- .../components/nibe_heatpump/config_flow.py | 154 ++++++++--- .../components/nibe_heatpump/const.py | 3 + .../components/nibe_heatpump/manifest.json | 2 +- .../components/nibe_heatpump/number.py | 4 + .../components/nibe_heatpump/select.py | 5 + .../components/nibe_heatpump/strings.json | 22 +- .../nibe_heatpump/translations/en.json | 22 +- requirements_all.txt | 5 +- requirements_test_all.txt | 5 +- .../nibe_heatpump/test_config_flow.py | 241 +++++++++++++----- 11 files changed, 394 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index 053d6db2a34cc4..68e16871549d8d 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -10,10 +10,10 @@ from nibe.coil import Coil from nibe.connection import Connection +from nibe.connection.modbus import Modbus from nibe.connection.nibegw import NibeGW from nibe.exceptions import CoilNotFoundException, CoilReadException -from nibe.heatpump import HeatPump, Model -from tenacity import RetryError, retry, retry_if_exception_type, stop_after_attempt +from nibe.heatpump import HeatPump, Model, Series from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -34,8 +34,11 @@ from .const import ( CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_MODBUS, CONF_CONNECTION_TYPE_NIBEGW, CONF_LISTENING_PORT, + CONF_MODBUS_UNIT, + CONF_MODBUS_URL, CONF_REMOTE_READ_PORT, CONF_REMOTE_WRITE_PORT, CONF_WORD_SWAP, @@ -57,12 +60,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nibe Heat Pump from a config entry.""" heatpump = HeatPump(Model[entry.data[CONF_MODEL]]) - heatpump.word_swap = entry.data[CONF_WORD_SWAP] - await hass.async_add_executor_job(heatpump.initialize) + await heatpump.initialize() + connection: Connection connection_type = entry.data[CONF_CONNECTION_TYPE] if connection_type == CONF_CONNECTION_TYPE_NIBEGW: + heatpump.word_swap = entry.data[CONF_WORD_SWAP] connection = NibeGW( heatpump, entry.data[CONF_IP_ADDRESS], @@ -70,13 +74,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_REMOTE_WRITE_PORT], listening_port=entry.data[CONF_LISTENING_PORT], ) + elif connection_type == CONF_CONNECTION_TYPE_MODBUS: + connection = Modbus( + heatpump, entry.data[CONF_MODBUS_URL], entry.data[CONF_MODBUS_UNIT] + ) else: raise HomeAssistantError(f"Connection type {connection_type} is not supported.") await connection.start() + assert heatpump.model + + async def _async_stop(_): + await connection.stop() + entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, connection.stop) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) ) coordinator = Coordinator(hass, heatpump, connection) @@ -184,6 +197,11 @@ def _on_coil_update(self, coil: Coil): self.seed[coil.address] = coil self.async_update_context_listeners([coil.address]) + @property + def series(self) -> Series: + """Return which series of pump we are connected to.""" + return self.heatpump.series + @property def coils(self) -> list[Coil]: """Return the full coil database.""" @@ -201,8 +219,8 @@ def device_info(self) -> DeviceInfo: def get_coil_value(self, coil: Coil) -> int | str | float | None: """Return a coil with data and check for validity.""" - if coil := self.data.get(coil.address): - return coil.value + if coil_with_data := self.data.get(coil.address): + return coil_with_data.value return None def get_coil_float(self, coil: Coil) -> float | None: @@ -228,33 +246,29 @@ async def _async_update_data(self) -> dict[int, Coil]: self.task = None async def _async_update_data_internal(self) -> dict[int, Coil]: - @retry( - retry=retry_if_exception_type(CoilReadException), - stop=stop_after_attempt(COIL_READ_RETRIES), - ) - async def read_coil(coil: Coil): - return await self.connection.read_coil(coil) result: dict[int, Coil] = {} - for address in self.context_callbacks.keys(): - if seed := self.seed.pop(address, None): - self.logger.debug("Skipping seeded coil: %d", address) - result[address] = seed - continue + def _get_coils() -> Iterable[Coil]: + for address in sorted(self.context_callbacks.keys()): + if seed := self.seed.pop(address, None): + self.logger.debug("Skipping seeded coil: %d", address) + result[address] = seed + continue - try: - coil = self.heatpump.get_coil_by_address(address) - except CoilNotFoundException as exception: - self.logger.debug("Skipping missing coil: %s", exception) - continue + try: + coil = self.heatpump.get_coil_by_address(address) + except CoilNotFoundException as exception: + self.logger.debug("Skipping missing coil: %s", exception) + continue + yield coil - try: - result[coil.address] = await read_coil(coil) - except (CoilReadException, RetryError) as exception: - raise UpdateFailed(f"Failed to update: {exception}") from exception - - self.seed.pop(coil.address, None) + try: + async for coil in self.connection.read_coils(_get_coils()): + result[coil.address] = coil + self.seed.pop(coil.address, None) + except CoilReadException as exception: + raise UpdateFailed(f"Failed to update: {exception}") from exception return result diff --git a/homeassistant/components/nibe_heatpump/config_flow.py b/homeassistant/components/nibe_heatpump/config_flow.py index d68def046fd528..6050010b20d012 100644 --- a/homeassistant/components/nibe_heatpump/config_flow.py +++ b/homeassistant/components/nibe_heatpump/config_flow.py @@ -1,14 +1,21 @@ """Config flow for Nibe Heat Pump integration.""" from __future__ import annotations -import errno -from socket import gaierror from typing import Any +from nibe.connection.modbus import Modbus from nibe.connection.nibegw import NibeGW -from nibe.exceptions import CoilNotFoundException, CoilReadException, CoilWriteException +from nibe.exceptions import ( + AddressInUseException, + CoilNotFoundException, + CoilReadException, + CoilReadSendException, + CoilWriteException, + CoilWriteSendException, +) from nibe.heatpump import HeatPump, Model import voluptuous as vol +import yarl from homeassistant import config_entries from homeassistant.const import CONF_IP_ADDRESS, CONF_MODEL @@ -18,8 +25,11 @@ from .const import ( CONF_CONNECTION_TYPE, + CONF_CONNECTION_TYPE_MODBUS, CONF_CONNECTION_TYPE_NIBEGW, CONF_LISTENING_PORT, + CONF_MODBUS_UNIT, + CONF_MODBUS_URL, CONF_REMOTE_READ_PORT, CONF_REMOTE_WRITE_PORT, CONF_WORD_SWAP, @@ -36,7 +46,7 @@ vol.Coerce(int), ) -STEP_USER_DATA_SCHEMA = vol.Schema( +STEP_NIBEGW_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_MODEL): vol.In(list(Model.__members__)), vol.Required(CONF_IP_ADDRESS): selector.TextSelector(), @@ -47,6 +57,22 @@ ) +STEP_MODBUS_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_MODEL): vol.In(list(Model.__members__)), + vol.Required(CONF_MODBUS_URL): selector.TextSelector(), + vol.Required(CONF_MODBUS_UNIT, default=0): vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=0, step=1, mode=selector.NumberSelectorMode.BOX + ) + ), + vol.Coerce(int), + ), + } +) + + class FieldError(Exception): """Field with invalid data.""" @@ -57,11 +83,13 @@ def __init__(self, message: str, field: str, error: str) -> None: self.error = error -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: +async def validate_nibegw_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, dict[str, Any]]: """Validate the user input allows us to connect.""" heatpump = HeatPump(Model[data[CONF_MODEL]]) - heatpump.initialize() + await heatpump.initialize() connection = NibeGW( heatpump, @@ -73,24 +101,17 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, try: await connection.start() - except OSError as exception: - if exception.errno == errno.EADDRINUSE: - raise FieldError( - "Address already in use", "listening_port", "address_in_use" - ) from exception - raise + except AddressInUseException as exception: + raise FieldError( + "Address already in use", "listening_port", "address_in_use" + ) from exception try: - coil = heatpump.get_coil_by_name("modbus40-word-swap-48852") - coil = await connection.read_coil(coil) - word_swap = coil.value == "ON" - coil = await connection.write_coil(coil) - except gaierror as exception: - raise FieldError(str(exception), "ip_address", "address") from exception + await connection.verify_connectivity() + except (CoilReadSendException, CoilWriteSendException) as exception: + raise FieldError(str(exception), CONF_IP_ADDRESS, "address") from exception except CoilNotFoundException as exception: - raise FieldError( - "Model selected doesn't seem to support expected coils", "base", "model" - ) from exception + raise FieldError("Coils not found", "base", "model") from exception except CoilReadException as exception: raise FieldError("Timeout on read from pump", "base", "read") from exception except CoilWriteException as exception: @@ -98,9 +119,49 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, finally: await connection.stop() - return { - "title": f"{data[CONF_MODEL]} at {data[CONF_IP_ADDRESS]}", - CONF_WORD_SWAP: word_swap, + return f"{data[CONF_MODEL]} at {data[CONF_IP_ADDRESS]}", { + **data, + CONF_WORD_SWAP: heatpump.word_swap, + CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_NIBEGW, + } + + +async def validate_modbus_input( + hass: HomeAssistant, data: dict[str, Any] +) -> tuple[str, dict[str, Any]]: + """Validate the user input allows us to connect.""" + + heatpump = HeatPump(Model[data[CONF_MODEL]]) + await heatpump.initialize() + + try: + connection = Modbus( + heatpump, + data[CONF_MODBUS_URL], + data[CONF_MODBUS_UNIT], + ) + except ValueError as exc: + raise FieldError("Not a valid modbus url", CONF_MODBUS_URL, "url") from exc + + await connection.start() + + try: + await connection.verify_connectivity() + except (CoilReadSendException, CoilWriteSendException) as exception: + raise FieldError(str(exception), CONF_MODBUS_URL, "address") from exception + except CoilNotFoundException as exception: + raise FieldError("Coils not found", "base", "model") from exception + except CoilReadException as exception: + raise FieldError("Timeout on read from pump", "base", "read") from exception + except CoilWriteException as exception: + raise FieldError("Timeout on writing to pump", "base", "write") from exception + finally: + await connection.stop() + + host = yarl.URL(data[CONF_MODBUS_URL]).host + return f"{data[CONF_MODEL]} at {host}", { + **data, + CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_MODBUS, } @@ -113,15 +174,21 @@ async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the initial step.""" + return self.async_show_menu(step_id="user", menu_options=["modbus", "nibegw"]) + + async def async_step_modbus( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the modbus step.""" if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="modbus", data_schema=STEP_MODBUS_DATA_SCHEMA ) errors = {} try: - info = await validate_input(self.hass, user_input) + title, data = await validate_modbus_input(self.hass, user_input) except FieldError as exception: LOGGER.debug("Validation error %s", exception) errors[exception.field] = exception.error @@ -129,13 +196,34 @@ async def async_step_user( LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - data = { - **user_input, - CONF_WORD_SWAP: info[CONF_WORD_SWAP], - CONF_CONNECTION_TYPE: CONF_CONNECTION_TYPE_NIBEGW, - } - return self.async_create_entry(title=info["title"], data=data) + return self.async_create_entry(title=title, data=data) + + return self.async_show_form( + step_id="modbus", data_schema=STEP_MODBUS_DATA_SCHEMA, errors=errors + ) + + async def async_step_nibegw( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the nibegw step.""" + if user_input is None: + return self.async_show_form( + step_id="nibegw", data_schema=STEP_NIBEGW_DATA_SCHEMA + ) + + errors = {} + + try: + title, data = await validate_nibegw_input(self.hass, user_input) + except FieldError as exception: + LOGGER.exception("Validation error") + errors[exception.field] = exception.error + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=title, data=data) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="nibegw", data_schema=STEP_NIBEGW_DATA_SCHEMA, errors=errors ) diff --git a/homeassistant/components/nibe_heatpump/const.py b/homeassistant/components/nibe_heatpump/const.py index f1bcbf111275bd..381ad7ba0c2d80 100644 --- a/homeassistant/components/nibe_heatpump/const.py +++ b/homeassistant/components/nibe_heatpump/const.py @@ -10,3 +10,6 @@ CONF_WORD_SWAP = "word_swap" CONF_CONNECTION_TYPE = "connection_type" CONF_CONNECTION_TYPE_NIBEGW = "nibegw" +CONF_CONNECTION_TYPE_MODBUS = "modbus" +CONF_MODBUS_URL = "modbus_url" +CONF_MODBUS_UNIT = "modbus_unit" diff --git a/homeassistant/components/nibe_heatpump/manifest.json b/homeassistant/components/nibe_heatpump/manifest.json index 4b66b93d31bb82..68dc8c7a06c09f 100644 --- a/homeassistant/components/nibe_heatpump/manifest.json +++ b/homeassistant/components/nibe_heatpump/manifest.json @@ -3,7 +3,7 @@ "name": "Nibe Heat Pump", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nibe_heatpump", - "requirements": ["nibe==0.5.0", "tenacity==8.0.1"], + "requirements": ["nibe==1.2.0"], "codeowners": ["@elupus"], "iot_class": "local_polling" } diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py index 11c6917ec1cd0d..606588f7142021 100644 --- a/homeassistant/components/nibe_heatpump/number.py +++ b/homeassistant/components/nibe_heatpump/number.py @@ -58,6 +58,10 @@ def __init__(self, coordinator: Coordinator, coil: Coil) -> None: self._attr_native_value = None def _async_read_coil(self, coil: Coil) -> None: + if coil.value is None: + self._attr_native_value = None + return + try: self._attr_native_value = float(coil.value) except ValueError: diff --git a/homeassistant/components/nibe_heatpump/select.py b/homeassistant/components/nibe_heatpump/select.py index 27df1980287cc9..412c1579586f47 100644 --- a/homeassistant/components/nibe_heatpump/select.py +++ b/homeassistant/components/nibe_heatpump/select.py @@ -35,11 +35,16 @@ class Select(CoilEntity, SelectEntity): def __init__(self, coordinator: Coordinator, coil: Coil) -> None: """Initialize entity.""" + assert coil.mappings super().__init__(coordinator, coil, ENTITY_ID_FORMAT) self._attr_options = list(coil.mappings.values()) self._attr_current_option = None def _async_read_coil(self, coil: Coil) -> None: + if not isinstance(coil.value, str): + self._attr_current_option = None + return + self._attr_current_option = coil.value async def async_select_option(self, option: str) -> None: diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json index 08a049cb17af66..d6e93af689ae81 100644 --- a/homeassistant/components/nibe_heatpump/strings.json +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -2,8 +2,27 @@ "config": { "step": { "user": { + "menu_options": { + "nibegw": "NibeGW", + "modbus": "Modbus" + }, + "description": "Pick the connection method to your pump. In general, F-series pumps require a Nibe GW custom accessory, while an S-series pump has Modbus support built-in." + }, + "modbus": { + "data": { + "model": "Model of Heat Pump", + "modbus_url": "Modbus URL", + "modbus_unit": "Modbus Unit Identifier" + }, + "data_description": { + "modbus_url": "Modbus URL that describes the connection to your Heat Pump or MODBUS40 unit. It should be on the form:\n - `tcp://[HOST]:[PORT]` for Modbus TCP connection\n - `serial://[LOCAL DEVICE]` for a local Modbus RTU connection\n - `rfc2217://[HOST]:[PORT]` for a remote telnet based Modbus RTU connection.", + "modbus_unit": "Unit identification for you Heat Pump. Can usually be left at 0." + } + }, + "nibegw": { "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory.", "data": { + "model": "Model of Heat Pump", "ip_address": "Remote address", "remote_read_port": "Remote read port", "remote_write_port": "Remote write port", @@ -23,7 +42,8 @@ "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.", "address_in_use": "The selected listening port is already in use on this system.", "model": "The model selected doesn't seem to support modbus40", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "url": "The url specified is not a well formed and supported url" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json index 4c6e86720f1ed1..167a8341cd3ec3 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -9,13 +9,26 @@ "model": "The model selected doesn't seem to support modbus40", "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", "unknown": "Unexpected error", + "url": "The url specified is not a well formed and supported url", "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus Unit Identifier", + "modbus_url": "Modbus URL", + "model": "Model of Heat Pump" + }, + "data_description": { + "modbus_unit": "Unit identification for you Heat Pump. Can usually be left at 0.", + "modbus_url": "Modbus URL that describes the connection to your Heat Pump or MODBUS40 unit. It should be on the form:\n - `tcp://[HOST]:[PORT]` for Modbus TCP connection\n - `serial://[LOCAL DEVICE]` for a local Modbus RTU connection\n - `rfc2217://[HOST]:[PORT]` for a remote telnet based Modbus RTU connection." + } + }, + "nibegw": { "data": { "ip_address": "Remote address", "listening_port": "Local listening port", + "model": "Model of Heat Pump", "remote_read_port": "Remote read port", "remote_write_port": "Remote write port" }, @@ -26,6 +39,13 @@ "remote_write_port": "The port the NibeGW unit is listening for write requests on." }, "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory." + }, + "user": { + "description": "Pick the connection method to your pump. In general, F-series pumps require a Nibe GW custom accessory, while an S-series pump has Modbus support built-in.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/requirements_all.txt b/requirements_all.txt index 61919196caf34e..d8243dad9c6010 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1159,7 +1159,7 @@ nextcord==2.0.0a8 nextdns==1.1.1 # homeassistant.components.nibe_heatpump -nibe==0.5.0 +nibe==1.2.0 # homeassistant.components.niko_home_control niko-home-control==0.2.1 @@ -2394,9 +2394,6 @@ temescal==0.5 # homeassistant.components.temper temperusb==1.6.0 -# homeassistant.components.nibe_heatpump -tenacity==8.0.1 - # homeassistant.components.tensorflow # tensorflow==2.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdb0a01bd23546..9270aa1a925921 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -846,7 +846,7 @@ nextcord==2.0.0a8 nextdns==1.1.1 # homeassistant.components.nibe_heatpump -nibe==0.5.0 +nibe==1.2.0 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 @@ -1652,9 +1652,6 @@ tellduslive==0.10.11 # homeassistant.components.lg_soundbar temescal==0.5 -# homeassistant.components.nibe_heatpump -tenacity==8.0.1 - # homeassistant.components.powerwall tesla-powerwall==0.3.18 diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index f7dc08c41bbf41..4a0751ea74bf09 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -1,11 +1,16 @@ """Test the Nibe Heat Pump config flow.""" -import errno -from socket import gaierror -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from nibe.coil import Coil from nibe.connection import Connection -from nibe.exceptions import CoilNotFoundException, CoilReadException, CoilWriteException +from nibe.exceptions import ( + AddressInUseException, + CoilNotFoundException, + CoilReadException, + CoilReadSendException, + CoilWriteException, +) +import pytest from pytest import fixture from homeassistant import config_entries @@ -13,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -MOCK_FLOW_USERDATA = { +MOCK_FLOW_NIBEGW_USERDATA = { "model": "F1155", "ip_address": "127.0.0.1", "listening_port": 9999, @@ -22,13 +27,33 @@ } -@fixture(autouse=True, name="mock_connection") -async def fixture_mock_connection(): +MOCK_FLOW_MODBUS_USERDATA = { + "model": "S1155", + "modbus_url": "tcp://127.0.0.1", + "modbus_unit": 0, +} + + +@fixture(autouse=True, name="mock_connection_constructor") +async def fixture_mock_connection_constructor(): """Make sure we have a dummy connection.""" + mock_constructor = Mock() with patch( - "homeassistant.components.nibe_heatpump.config_flow.NibeGW", spec=Connection - ) as mock_connection: - yield mock_connection + "homeassistant.components.nibe_heatpump.config_flow.NibeGW", + new=mock_constructor, + ), patch( + "homeassistant.components.nibe_heatpump.config_flow.Modbus", + new=mock_constructor, + ): + yield mock_constructor + + +@fixture(name="mock_connection") +def fixture_mock_connection(mock_connection_constructor: Mock): + """Make sure we have a dummy connection.""" + mock_connection = AsyncMock(spec=Connection) + mock_connection_constructor.return_value = mock_connection + return mock_connection @fixture(autouse=True, name="mock_setup_entry") @@ -40,24 +65,38 @@ async def fixture_mock_setup(): yield mock_setup_entry -async def test_form( - hass: HomeAssistant, mock_connection: Mock, mock_setup_entry: Mock -) -> None: +async def _get_connection_form( + hass: HomeAssistant, connection_type: str +) -> FlowResultType: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == FlowResultType.MENU + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"next_step_id": connection_type} + ) + assert result["type"] == FlowResultType.FORM assert result["errors"] is None + return result + + +async def test_nibegw_form( + hass: HomeAssistant, mock_connection: Mock, mock_setup_entry: Mock +) -> None: + """Test we get the form.""" + result = await _get_connection_form(hass, "nibegw") coil_wordswap = Coil( 48852, "modbus40-word-swap-48852", "Modbus40 Word Swap", "u8", min=0, max=1 ) coil_wordswap.value = "ON" - mock_connection.return_value.read_coil.return_value = coil_wordswap + mock_connection.read_coil.return_value = coil_wordswap result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA + result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) await hass.async_block_till_done() @@ -75,109 +114,175 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 -async def test_address_inuse(hass: HomeAssistant, mock_connection: Mock) -> None: +async def test_modbus_form( + hass: HomeAssistant, mock_connection: Mock, mock_setup_entry: Mock +) -> None: + """Test we get the form.""" + result = await _get_connection_form(hass, "modbus") + + coil = Coil( + 40022, "reset-alarm-40022", "Reset Alarm", "u8", min=0, max=1, write=True + ) + coil.value = "ON" + mock_connection.read_coil.return_value = coil + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_FLOW_MODBUS_USERDATA + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "S1155 at 127.0.0.1" + assert result2["data"] == { + "model": "S1155", + "modbus_url": "tcp://127.0.0.1", + "modbus_unit": 0, + "connection_type": "modbus", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_modbus_invalid_url( + hass: HomeAssistant, mock_connection_constructor: Mock +) -> None: """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + result = await _get_connection_form(hass, "modbus") + + mock_connection_constructor.side_effect = ValueError() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**MOCK_FLOW_MODBUS_USERDATA, "modbus_url": "invalid://url"} ) - error = OSError() - error.errno = errno.EADDRINUSE - mock_connection.return_value.start.side_effect = error + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"modbus_url": "url"} + + +async def test_nibegw_address_inuse(hass: HomeAssistant, mock_connection: Mock) -> None: + """Test we handle invalid auth.""" + result = await _get_connection_form(hass, "nibegw") + + mock_connection.start.side_effect = AddressInUseException() result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA + result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"listening_port": "address_in_use"} - error.errno = errno.EACCES - mock_connection.return_value.start.side_effect = error + mock_connection.start.side_effect = Exception() result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA + result["flow_id"], MOCK_FLOW_NIBEGW_USERDATA ) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} -async def test_read_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_read_timeout( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = CoilReadException() + mock_connection.verify_connectivity.side_effect = CoilReadException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "read"} -async def test_write_timeout(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_write_timeout( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.write_coil.side_effect = CoilWriteException() + mock_connection.verify_connectivity.side_effect = CoilWriteException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "write"} -async def test_unexpected_exception(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_unexpected_exception( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = Exception() + mock_connection.verify_connectivity.side_effect = Exception() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], MOCK_FLOW_USERDATA - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} -async def test_invalid_host(hass: HomeAssistant, mock_connection: Mock) -> None: +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_nibegw_invalid_host( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = gaierror() + mock_connection.verify_connectivity.side_effect = CoilReadSendException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {**MOCK_FLOW_USERDATA, "ip_address": "abcd"} - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"ip_address": "address"} - - -async def test_model_missing_coil(hass: HomeAssistant, mock_connection: Mock) -> None: + if connection_type == "nibegw": + assert result2["errors"] == {"ip_address": "address"} + else: + assert result2["errors"] == {"modbus_url": "address"} + + +@pytest.mark.parametrize( + "connection_type,data", + ( + ("nibegw", MOCK_FLOW_NIBEGW_USERDATA), + ("modbus", MOCK_FLOW_MODBUS_USERDATA), + ), +) +async def test_model_missing_coil( + hass: HomeAssistant, mock_connection: Mock, connection_type: str, data: dict +) -> None: """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + result = await _get_connection_form(hass, connection_type) - mock_connection.return_value.read_coil.side_effect = CoilNotFoundException() + mock_connection.verify_connectivity.side_effect = CoilNotFoundException() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {**MOCK_FLOW_USERDATA} - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data) assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "model"} From 274049cc8edeb82b737bbf92a094fd2f0e4de1e6 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 8 Nov 2022 11:02:53 +0100 Subject: [PATCH 302/394] Fix ignored upnp discoveries not being matched when device changes its unique identifier (#81240) Fixes https://github.com/home-assistant/core/issues/78454 fixes undefined --- homeassistant/components/upnp/config_flow.py | 60 ++++++++++++++------ homeassistant/components/upnp/const.py | 1 + tests/components/upnp/test_config_flow.py | 38 +++++++++++++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 6b48839846142a..b7d6425707db4c 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -20,6 +20,7 @@ CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DOMAIN, + DOMAIN_DISCOVERIES, LOGGER, ST_IGD_V1, ST_IGD_V2, @@ -47,7 +48,7 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: ) -async def _async_discover_igd_devices( +async def _async_discovered_igd_devices( hass: HomeAssistant, ) -> list[ssdp.SsdpServiceInfo]: """Discovery IGD devices.""" @@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() # - user(None): scan --> user({...}) --> create_entry() - def __init__(self) -> None: - """Initialize the UPnP/IGD config flow.""" - self._discoveries: list[SsdpServiceInfo] | None = None + @property + def _discoveries(self) -> dict[str, SsdpServiceInfo]: + """Get current discoveries.""" + domain_data: dict = self.hass.data.setdefault(DOMAIN, {}) + return domain_data.setdefault(DOMAIN_DISCOVERIES, {}) + + def _add_discovery(self, discovery: SsdpServiceInfo) -> None: + """Add a discovery.""" + self._discoveries[discovery.ssdp_usn] = discovery + + def _remove_discovery(self, usn: str) -> SsdpServiceInfo: + """Remove a discovery by its USN/unique_id.""" + return self._discoveries.pop(usn) async def async_step_user( self, user_input: Mapping[str, Any] | None = None @@ -95,7 +106,7 @@ async def async_step_user( discovery = next( iter( discovery - for discovery in self._discoveries + for discovery in self._discoveries.values() if discovery.ssdp_usn == user_input["unique_id"] ) ) @@ -103,21 +114,19 @@ async def async_step_user( return await self._async_create_entry_from_discovery(discovery) # Discover devices. - discoveries = await _async_discover_igd_devices(self.hass) + discoveries = await _async_discovered_igd_devices(self.hass) # Store discoveries which have not been configured. current_unique_ids = { entry.unique_id for entry in self._async_current_entries() } - self._discoveries = [ - discovery - for discovery in discoveries + for discovery in discoveries: if ( _is_complete_discovery(discovery) and _is_igd_device(discovery) and discovery.ssdp_usn not in current_unique_ids - ) - ] + ): + self._add_discovery(discovery) # Ensure anything to add. if not self._discoveries: @@ -128,7 +137,7 @@ async def async_step_user( vol.Required("unique_id"): vol.In( { discovery.ssdp_usn: _friendly_name_from_discovery(discovery) - for discovery in self._discoveries + for discovery in self._discoveries.values() } ), } @@ -163,12 +172,13 @@ async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowRes mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info) host = discovery_info.ssdp_headers["_host"] self._abort_if_unique_id_configured( - # Store mac address for older entries. + # Store mac address and other data for older entries. # The location is stored in the config entry such that when the location changes, the entry is reloaded. updates={ CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location, CONFIG_ENTRY_HOST: host, + CONFIG_ENTRY_ST: discovery_info.ssdp_st, }, ) @@ -204,7 +214,7 @@ async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowRes return self.async_abort(reason="config_entry_updated") # Store discovery. - self._discoveries = [discovery_info] + self._add_discovery(discovery_info) # Ensure user recognizable. self.context["title_placeholders"] = { @@ -221,10 +231,27 @@ async def async_step_ssdp_confirm( if user_input is None: return self.async_show_form(step_id="ssdp_confirm") - assert self._discoveries - discovery = self._discoveries[0] + assert self.unique_id + discovery = self._remove_discovery(self.unique_id) return await self._async_create_entry_from_discovery(discovery) + async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult: + """Ignore this config flow.""" + usn = user_input["unique_id"] + discovery = self._remove_discovery(usn) + mac_address = await _async_mac_address_from_discovery(self.hass, discovery) + data = { + CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_ST: discovery.ssdp_st, + CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_MAC_ADDRESS: mac_address, + CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], + CONFIG_ENTRY_LOCATION: discovery.ssdp_location, + } + + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title=user_input["title"], data=data) + async def _async_create_entry_from_discovery( self, discovery: SsdpServiceInfo, @@ -243,5 +270,6 @@ async def _async_create_entry_from_discovery( CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], CONFIG_ENTRY_LOCATION: discovery.ssdp_location, CONFIG_ENTRY_MAC_ADDRESS: mac_address, + CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], } return self.async_create_entry(title=title, data=data) diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 8d98790983a41c..5f73b1e63c9cdc 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -7,6 +7,7 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "upnp" +DOMAIN_DISCOVERIES = "discoveries" BYTES_RECEIVED = "bytes_received" BYTES_SENT = "bytes_sent" PACKETS_RECEIVED = "packets_received" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index f0a1de1ce37d56..7850554f751a17 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -63,6 +63,42 @@ async def test_flow_ssdp(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, + } + + +@pytest.mark.usefixtures( + "ssdp_instant_discovery", + "mock_setup_entry", + "mock_get_source_ip", + "mock_mac_address_from_host", +) +async def test_flow_ssdp_ignore(hass: HomeAssistant): + """Test config flow: discovered + ignore through ssdp.""" + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=TEST_DISCOVERY, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "ssdp_confirm" + + # Ignore entry. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": TEST_USN, "title": TEST_FRIENDLY_NAME}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, } @@ -138,6 +174,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: None, + CONFIG_ENTRY_HOST: TEST_HOST, } @@ -382,6 +419,7 @@ async def test_flow_user(hass: HomeAssistant): CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, CONFIG_ENTRY_LOCATION: TEST_LOCATION, CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, } From 462e2a8ea1a9461389b2c286c072d38e0738aad2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:03:37 -0600 Subject: [PATCH 303/394] Fix HomeKit reset accessory procedure (#81573) fixes https://github.com/home-assistant/core/issues/81571 --- homeassistant/components/homekit/__init__.py | 28 ++++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index b809f6db205a40..333d6052d175fb 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -10,8 +10,10 @@ from typing import Any, cast from aiohttp import web +from pyhap.characteristic import Characteristic from pyhap.const import STANDALONE_AID from pyhap.loader import get_loader +from pyhap.service import Service import voluptuous as vol from zeroconf.asyncio import AsyncZeroconf @@ -139,7 +141,7 @@ PORT_CLEANUP_CHECK_INTERVAL_SECS = 1 _HOMEKIT_CONFIG_UPDATE_TIME = ( - 5 # number of seconds to wait for homekit to see the c# change + 10 # number of seconds to wait for homekit to see the c# change ) @@ -529,6 +531,7 @@ def __init__( self.status = STATUS_READY self.driver: HomeDriver | None = None self.bridge: HomeBridge | None = None + self._reset_lock = asyncio.Lock() def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: """Set up bridge and accessory driver.""" @@ -558,21 +561,24 @@ def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None: """Reset the accessory to load the latest configuration.""" - if not self.bridge: - await self.async_reset_accessories_in_accessory_mode(entity_ids) - return - await self.async_reset_accessories_in_bridge_mode(entity_ids) + async with self._reset_lock: + if not self.bridge: + await self.async_reset_accessories_in_accessory_mode(entity_ids) + return + await self.async_reset_accessories_in_bridge_mode(entity_ids) async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None: """Shutdown an accessory.""" assert self.driver is not None await accessory.stop() # Deallocate the IIDs for the accessory - iid_manager = self.driver.iid_manager - for service in accessory.services: - iid_manager.remove_iid(iid_manager.remove_obj(service)) - for char in service.characteristics: - iid_manager.remove_iid(iid_manager.remove_obj(char)) + iid_manager = accessory.iid_manager + services: list[Service] = accessory.services + for service in services: + iid_manager.remove_obj(service) + characteristics: list[Characteristic] = service.characteristics + for char in characteristics: + iid_manager.remove_obj(char) async def async_reset_accessories_in_accessory_mode( self, entity_ids: Iterable[str] @@ -581,7 +587,6 @@ async def async_reset_accessories_in_accessory_mode( assert self.driver is not None acc = cast(HomeAccessory, self.driver.accessory) - await self._async_shutdown_accessory(acc) if acc.entity_id not in entity_ids: return if not (state := self.hass.states.get(acc.entity_id)): @@ -589,6 +594,7 @@ async def async_reset_accessories_in_accessory_mode( "The underlying entity %s disappeared during reset", acc.entity_id ) return + await self._async_shutdown_accessory(acc) if new_acc := self._async_create_single_accessory([state]): self.driver.accessory = new_acc self.hass.async_add_job(new_acc.run) From 11034f56dc2167e14f31d93128f8d46d748baef1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:04:24 -0600 Subject: [PATCH 304/394] Ensure HomeKit temperature controls appear before fan controls on thermostat accessories (#81586) --- homeassistant/components/homekit/type_thermostats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index a8c7a53718ae0b..a924548816b745 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -306,7 +306,7 @@ def __init__(self, *args): if attributes.get(ATTR_HVAC_ACTION) is not None: self.fan_chars.append(CHAR_CURRENT_FAN_STATE) serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars) - serv_fan.add_linked_service(serv_thermostat) + serv_thermostat.add_linked_service(serv_fan) self.char_active = serv_fan.configure_char( CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active ) From 53c1c2eb56558eb934ee37efbd8ceb59bd702cab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:15:16 -0600 Subject: [PATCH 305/394] Fix homekit bridge iid allocations (#81613) fixes undefined --- homeassistant/components/homekit/__init__.py | 10 +- .../components/homekit/accessories.py | 8 +- .../components/homekit/diagnostics.py | 2 + .../components/homekit/iidmanager.py | 72 ++++- tests/components/homekit/conftest.py | 8 +- tests/components/homekit/fixtures/iids_v1 | 249 ++++++++++++++++ .../homekit/fixtures/iids_v1_with_underscore | 50 ++++ tests/components/homekit/fixtures/iids_v2 | 273 ++++++++++++++++++ .../homekit/fixtures/iids_v2_with_underscore | 54 ++++ tests/components/homekit/test_accessories.py | 22 +- tests/components/homekit/test_diagnostics.py | 28 ++ tests/components/homekit/test_homekit.py | 6 +- tests/components/homekit/test_iidmanager.py | 83 +++++- 13 files changed, 806 insertions(+), 59 deletions(-) create mode 100644 tests/components/homekit/fixtures/iids_v1 create mode 100644 tests/components/homekit/fixtures/iids_v1_with_underscore create mode 100644 tests/components/homekit/fixtures/iids_v2 create mode 100644 tests/components/homekit/fixtures/iids_v2_with_underscore diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 333d6052d175fb..ca73c7dc242675 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -76,13 +76,7 @@ type_switches, type_thermostats, ) -from .accessories import ( - HomeAccessory, - HomeBridge, - HomeDriver, - HomeIIDManager, - get_accessory, -) +from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory from .aidmanager import AccessoryAidStorage from .const import ( ATTR_INTEGRATION, @@ -551,7 +545,7 @@ def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: async_zeroconf_instance=async_zeroconf_instance, zeroconf_server=f"{uuid}-hap.local.", loader=get_loader(), - iid_manager=HomeIIDManager(self.iid_storage), + iid_storage=self.iid_storage, ) # If we do not load the mac address will be wrong diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 7d0de1a5740132..3832d9d31c27c8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -274,7 +274,7 @@ def __init__( driver=driver, display_name=cleanup_name_for_homekit(name), aid=aid, - iid_manager=driver.iid_manager, + iid_manager=HomeIIDManager(driver.iid_storage), *args, **kwargs, ) @@ -574,7 +574,7 @@ class HomeBridge(Bridge): # type: ignore[misc] def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None: """Initialize a Bridge object.""" - super().__init__(driver, name, iid_manager=driver.iid_manager) + super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage)) self.set_info_service( firmware_revision=format_version(__version__), manufacturer=MANUFACTURER, @@ -607,7 +607,7 @@ def __init__( entry_id: str, bridge_name: str, entry_title: str, - iid_manager: HomeIIDManager, + iid_storage: AccessoryIIDStorage, **kwargs: Any, ) -> None: """Initialize a AccessoryDriver object.""" @@ -616,7 +616,7 @@ def __init__( self._entry_id = entry_id self._bridge_name = bridge_name self._entry_title = entry_title - self.iid_manager = iid_manager + self.iid_storage = iid_storage @pyhap_callback # type: ignore[misc] def pair( diff --git a/homeassistant/components/homekit/diagnostics.py b/homeassistant/components/homekit/diagnostics.py index dbd40c1d6f50da..1d0bfb92fcc44a 100644 --- a/homeassistant/components/homekit/diagnostics.py +++ b/homeassistant/components/homekit/diagnostics.py @@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics( "options": dict(entry.options), }, } + if homekit.iid_storage: + data["iid_storage"] = homekit.iid_storage.allocations if not homekit.driver: # not started yet or startup failed return data driver: AccessoryDriver = homekit.driver diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py index 1b5cc7d6722952..3805748225a0cb 100644 --- a/homeassistant/components/homekit/iidmanager.py +++ b/homeassistant/components/homekit/iidmanager.py @@ -17,7 +17,7 @@ from .util import get_iid_storage_filename_for_entry_id -IID_MANAGER_STORAGE_VERSION = 1 +IID_MANAGER_STORAGE_VERSION = 2 IID_MANAGER_SAVE_DELAY = 2 ALLOCATIONS_KEY = "allocations" @@ -26,6 +26,40 @@ IID_MAX = 18446744073709551615 +ACCESSORY_INFORMATION_SERVICE = "3E" + + +class IIDStorage(Store): + """Storage class for IIDManager.""" + + async def _async_migrate_func( + self, + old_major_version: int, + old_minor_version: int, + old_data: dict, + ): + """Migrate to the new version.""" + if old_major_version == 1: + # Convert v1 to v2 format which uses a unique iid set per accessory + # instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE + # to always have iid 1 for each bridged accessory as well as the bridge + old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {}) + new_allocation: dict[str, dict[str, int]] = {} + old_data[ALLOCATIONS_KEY] = new_allocation + for allocation_key, iid in old_allocations.items(): + aid_str, new_allocation_key = allocation_key.split("_", 1) + service_type, _, char_type, *_ = new_allocation_key.split("_") + accessory_allocation = new_allocation.setdefault(aid_str, {}) + if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type: + accessory_allocation[new_allocation_key] = 1 + elif iid != 1: + accessory_allocation[new_allocation_key] = iid + + return old_data + + raise NotImplementedError + + class AccessoryIIDStorage: """ Provide stable allocation of IIDs for the lifetime of an accessory. @@ -37,15 +71,15 @@ class AccessoryIIDStorage: def __init__(self, hass: HomeAssistant, entry_id: str) -> None: """Create a new iid store.""" self.hass = hass - self.allocations: dict[str, int] = {} - self.allocated_iids: list[int] = [] + self.allocations: dict[str, dict[str, int]] = {} + self.allocated_iids: dict[str, list[int]] = {} self.entry_id = entry_id - self.store: Store | None = None + self.store: IIDStorage | None = None async def async_initialize(self) -> None: """Load the latest IID data.""" iid_store = get_iid_storage_filename_for_entry_id(self.entry_id) - self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store) + self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store) if not (raw_storage := await self.store.async_load()): # There is no data about iid allocations yet @@ -53,7 +87,8 @@ async def async_initialize(self) -> None: assert isinstance(raw_storage, dict) self.allocations = raw_storage.get(ALLOCATIONS_KEY, {}) - self.allocated_iids = sorted(self.allocations.values()) + for aid_str, allocations in self.allocations.items(): + self.allocated_iids[aid_str] = sorted(allocations.values()) def get_or_allocate_iid( self, @@ -68,16 +103,25 @@ def get_or_allocate_iid( char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None # Allocation key must be a string since we are saving it to JSON allocation_key = ( - f'{aid}_{service_hap_type}_{service_unique_id or ""}_' + f'{service_hap_type}_{service_unique_id or ""}_' f'{char_hap_type or ""}_{char_unique_id or ""}' ) - if allocation_key in self.allocations: - return self.allocations[allocation_key] - next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1 - self.allocations[allocation_key] = next_iid - self.allocated_iids.append(next_iid) + # AID must be a string since JSON keys cannot be int + aid_str = str(aid) + accessory_allocation = self.allocations.setdefault(aid_str, {}) + accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, []) + if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None: + allocated_iid = 1 + elif allocation_key in accessory_allocation: + return accessory_allocation[allocation_key] + elif accessory_allocated_iids: + allocated_iid = accessory_allocated_iids[-1] + 1 + else: + allocated_iid = 2 + accessory_allocation[allocation_key] = allocated_iid + accessory_allocated_iids.append(allocated_iid) self._async_schedule_save() - return next_iid + return allocated_iid @callback def _async_schedule_save(self) -> None: @@ -91,6 +135,6 @@ async def async_save(self) -> None: return await self.store.async_save(self._data_to_save()) @callback - def _data_to_save(self) -> dict[str, dict[str, int]]: + def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]: """Return data of entity map to store in a file.""" return {ALLOCATIONS_KEY: self.allocations} diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 7b79e0f9b6b6f2..b0422a40f7286e 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.device_tracker.legacy import YAML_DEVICES -from homeassistant.components.homekit.accessories import HomeDriver, HomeIIDManager +from homeassistant.components.homekit.accessories import HomeDriver from homeassistant.components.homekit.const import BRIDGE_NAME, EVENT_HOMEKIT_CHANGED from homeassistant.components.homekit.iidmanager import AccessoryIIDStorage @@ -39,7 +39,7 @@ def run_driver(hass, loop, iid_storage): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) @@ -63,7 +63,7 @@ def hk_driver(hass, loop, iid_storage): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) @@ -91,7 +91,7 @@ def mock_hap(hass, loop, iid_storage, mock_zeroconf): entry_id="", entry_title="mock entry", bridge_name=BRIDGE_NAME, - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address="127.0.0.1", loop=loop, ) diff --git a/tests/components/homekit/fixtures/iids_v1 b/tests/components/homekit/fixtures/iids_v1 new file mode 100644 index 00000000000000..1da11d8de6714c --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v1 @@ -0,0 +1,249 @@ +{ + "version": 1, + "minor_version": 1, + "key": "homekit.v1.iids", + "data": { + "allocations": { + "1_3E___": 1, + "1_3E__14_": 2, + "1_3E__20_": 3, + "1_3E__21_": 4, + "1_3E__23_": 5, + "1_3E__30_": 6, + "1_3E__52_": 7, + "1_A2___": 8, + "1_A2__37_": 9, + "935391877_3E___": 10, + "935391877_3E__14_": 11, + "935391877_3E__20_": 12, + "935391877_3E__21_": 13, + "935391877_3E__23_": 14, + "935391877_3E__30_": 15, + "935391877_3E__52_": 16, + "935391877_4A___": 17, + "935391877_4A__F_": 18, + "935391877_4A__33_": 19, + "935391877_4A__11_": 20, + "935391877_4A__35_": 21, + "935391877_4A__36_": 22, + "935391877_4A__D_": 23, + "935391877_4A__12_": 24, + "935391877_4A__34_": 25, + "935391877_4A__10_": 26, + "935391877_B7___": 27, + "935391877_B7__B0_": 28, + "935391877_B7__BF_": 29, + "935391877_B7__AF_": 30, + "985724734_3E___": 31, + "985724734_3E__14_": 32, + "985724734_3E__20_": 33, + "985724734_3E__21_": 34, + "985724734_3E__23_": 35, + "985724734_3E__30_": 36, + "985724734_3E__52_": 37, + "985724734_4A___": 38, + "985724734_4A__F_": 39, + "985724734_4A__33_": 40, + "985724734_4A__11_": 41, + "985724734_4A__35_": 42, + "985724734_4A__36_": 43, + "985724734_4A__D_": 44, + "985724734_4A__12_": 45, + "985724734_4A__34_": 46, + "985724734_4A__10_": 47, + "985724734_B7___": 48, + "985724734_B7__B0_": 49, + "985724734_B7__BF_": 50, + "985724734_B7__AF_": 51, + "3083074204_3E___": 52, + "3083074204_3E__14_": 53, + "3083074204_3E__20_": 54, + "3083074204_3E__21_": 55, + "3083074204_3E__23_": 56, + "3083074204_3E__30_": 57, + "3083074204_3E__52_": 58, + "3083074204_4A___": 59, + "3083074204_4A__F_": 60, + "3083074204_4A__33_": 61, + "3083074204_4A__11_": 62, + "3083074204_4A__35_": 63, + "3083074204_4A__36_": 64, + "3083074204_4A__D_": 65, + "3083074204_4A__12_": 66, + "3083074204_4A__34_": 67, + "3083074204_4A__10_": 68, + "3083074204_B7___": 69, + "3083074204_B7__B0_": 70, + "3083074204_B7__BF_": 71, + "3083074204_B7__AF_": 72, + "3032741347_3E___": 73, + "3032741347_3E__14_": 74, + "3032741347_3E__20_": 75, + "3032741347_3E__21_": 76, + "3032741347_3E__23_": 77, + "3032741347_3E__30_": 78, + "3032741347_3E__52_": 79, + "3032741347_4A___": 80, + "3032741347_4A__F_": 81, + "3032741347_4A__33_": 82, + "3032741347_4A__11_": 83, + "3032741347_4A__35_": 84, + "3032741347_4A__36_": 85, + "3032741347_4A__D_": 86, + "3032741347_4A__12_": 87, + "3032741347_4A__34_": 88, + "3032741347_4A__10_": 89, + "3032741347_B7___": 90, + "3032741347_B7__B0_": 91, + "3032741347_B7__BF_": 92, + "3032741347_B7__AF_": 93, + "3681509609_3E___": 94, + "3681509609_3E__14_": 95, + "3681509609_3E__20_": 96, + "3681509609_3E__21_": 97, + "3681509609_3E__23_": 98, + "3681509609_3E__30_": 99, + "3681509609_3E__52_": 100, + "3681509609_4A___": 101, + "3681509609_4A__F_": 102, + "3681509609_4A__33_": 103, + "3681509609_4A__11_": 104, + "3681509609_4A__35_": 105, + "3681509609_4A__36_": 106, + "3681509609_4A__D_": 107, + "3681509609_4A__12_": 108, + "3681509609_4A__34_": 109, + "3681509609_4A__10_": 110, + "3681509609_B7___": 111, + "3681509609_B7__B0_": 112, + "3681509609_B7__BF_": 113, + "3681509609_B7__AF_": 114, + "3866063418_3E___": 115, + "3866063418_3E__14_": 116, + "3866063418_3E__20_": 117, + "3866063418_3E__21_": 118, + "3866063418_3E__23_": 119, + "3866063418_3E__30_": 120, + "3866063418_3E__52_": 121, + "3866063418_4A___": 122, + "3866063418_4A__F_": 123, + "3866063418_4A__33_": 124, + "3866063418_4A__11_": 125, + "3866063418_4A__35_": 126, + "3866063418_4A__36_": 127, + "3866063418_4A__D_": 128, + "3866063418_4A__12_": 129, + "3866063418_4A__34_": 130, + "3866063418_4A__10_": 131, + "3866063418_B7___": 132, + "3866063418_B7__B0_": 133, + "3866063418_B7__BF_": 134, + "3866063418_B7__AF_": 135, + "3239498961_3E___": 136, + "3239498961_3E__14_": 137, + "3239498961_3E__20_": 138, + "3239498961_3E__21_": 139, + "3239498961_3E__23_": 140, + "3239498961_3E__30_": 141, + "3239498961_3E__52_": 142, + "3239498961_4A___": 143, + "3239498961_4A__F_": 144, + "3239498961_4A__33_": 145, + "3239498961_4A__11_": 146, + "3239498961_4A__35_": 147, + "3239498961_4A__36_": 148, + "3239498961_4A__D_": 149, + "3239498961_4A__12_": 150, + "3239498961_4A__34_": 151, + "3239498961_4A__10_": 152, + "3239498961_B7___": 153, + "3239498961_B7__B0_": 154, + "3239498961_B7__BF_": 155, + "3239498961_B7__AF_": 156, + "3289831818_3E___": 157, + "3289831818_3E__14_": 158, + "3289831818_3E__20_": 159, + "3289831818_3E__21_": 160, + "3289831818_3E__23_": 161, + "3289831818_3E__30_": 162, + "3289831818_3E__52_": 163, + "3289831818_4A___": 164, + "3289831818_4A__F_": 165, + "3289831818_4A__33_": 166, + "3289831818_4A__11_": 167, + "3289831818_4A__35_": 168, + "3289831818_4A__36_": 169, + "3289831818_4A__D_": 170, + "3289831818_4A__12_": 171, + "3289831818_4A__34_": 172, + "3289831818_4A__10_": 173, + "3289831818_B7___": 174, + "3289831818_B7__B0_": 175, + "3289831818_B7__BF_": 176, + "3289831818_B7__AF_": 177, + "3071722771_3E___": 178, + "3071722771_3E__14_": 179, + "3071722771_3E__20_": 180, + "3071722771_3E__21_": 181, + "3071722771_3E__23_": 182, + "3071722771_3E__30_": 183, + "3071722771_3E__52_": 184, + "3071722771_4A___": 185, + "3071722771_4A__F_": 186, + "3071722771_4A__33_": 187, + "3071722771_4A__11_": 188, + "3071722771_4A__35_": 189, + "3071722771_4A__36_": 190, + "3071722771_4A__D_": 191, + "3071722771_4A__12_": 192, + "3071722771_4A__34_": 193, + "3071722771_4A__10_": 194, + "3071722771_B7___": 195, + "3071722771_B7__B0_": 196, + "3071722771_B7__BF_": 197, + "3071722771_B7__AF_": 198, + "3391630365_3E___": 199, + "3391630365_3E__14_": 200, + "3391630365_3E__20_": 201, + "3391630365_3E__21_": 202, + "3391630365_3E__23_": 203, + "3391630365_3E__30_": 204, + "3391630365_3E__52_": 205, + "3391630365_4A___": 206, + "3391630365_4A__F_": 207, + "3391630365_4A__33_": 208, + "3391630365_4A__11_": 209, + "3391630365_4A__35_": 210, + "3391630365_4A__36_": 211, + "3391630365_4A__D_": 212, + "3391630365_4A__12_": 213, + "3391630365_4A__34_": 214, + "3391630365_4A__10_": 215, + "3391630365_B7___": 216, + "3391630365_B7__B0_": 217, + "3391630365_B7__BF_": 218, + "3391630365_B7__AF_": 219, + "3274187032_3E___": 220, + "3274187032_3E__14_": 221, + "3274187032_3E__20_": 222, + "3274187032_3E__21_": 223, + "3274187032_3E__23_": 224, + "3274187032_3E__30_": 225, + "3274187032_3E__52_": 226, + "3274187032_4A___": 227, + "3274187032_4A__F_": 228, + "3274187032_4A__33_": 229, + "3274187032_4A__11_": 230, + "3274187032_4A__35_": 231, + "3274187032_4A__36_": 232, + "3274187032_4A__D_": 233, + "3274187032_4A__12_": 234, + "3274187032_4A__34_": 235, + "3274187032_4A__10_": 236, + "3274187032_B7___": 237, + "3274187032_B7__B0_": 238, + "3274187032_B7__BF_": 239, + "3274187032_B7__AF_": 240 + } + } +} diff --git a/tests/components/homekit/fixtures/iids_v1_with_underscore b/tests/components/homekit/fixtures/iids_v1_with_underscore new file mode 100644 index 00000000000000..844c17a474680e --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v1_with_underscore @@ -0,0 +1,50 @@ +{ + "version": 1, + "minor_version": 1, + "key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids", + "data": { + "allocations": { + "1_3E___": 1, + "1_3E__14_": 2, + "1_3E__20_": 3, + "1_3E__21_": 4, + "1_3E__23_": 5, + "1_3E__30_": 6, + "1_3E__52_": 7, + "1_A2___": 8, + "1_A2__37_": 9, + "1973560704_3E___": 10, + "1973560704_3E__14_": 11, + "1973560704_3E__20_": 12, + "1973560704_3E__21_": 13, + "1973560704_3E__23_": 14, + "1973560704_3E__30_": 15, + "1973560704_3E__52_": 16, + "1973560704_3E__53_": 17, + "1973560704_89_pressed-__": 18, + "1973560704_89_pressed-_73_": 19, + "1973560704_89_pressed-_23_": 20, + "1973560704_89_pressed-_CB_": 21, + "1973560704_CC_pressed-__": 22, + "1973560704_CC_pressed-_CD_": 23, + "1973560704_89_changed_states-__": 24, + "1973560704_89_changed_states-_73_": 25, + "1973560704_89_changed_states-_23_": 26, + "1973560704_89_changed_states-_CB_": 27, + "1973560704_CC_changed_states-__": 28, + "1973560704_CC_changed_states-_CD_": 29, + "1973560704_89_turned_off-__": 30, + "1973560704_89_turned_off-_73_": 31, + "1973560704_89_turned_off-_23_": 32, + "1973560704_89_turned_off-_CB_": 33, + "1973560704_CC_turned_off-__": 34, + "1973560704_CC_turned_off-_CD_": 35, + "1973560704_89_turned_on-__": 36, + "1973560704_89_turned_on-_73_": 37, + "1973560704_89_turned_on-_23_": 38, + "1973560704_89_turned_on-_CB_": 39, + "1973560704_CC_turned_on-__": 40, + "1973560704_CC_turned_on-_CD_": 41 + } + } +} \ No newline at end of file diff --git a/tests/components/homekit/fixtures/iids_v2 b/tests/components/homekit/fixtures/iids_v2 new file mode 100644 index 00000000000000..76bff55e935445 --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v2 @@ -0,0 +1,273 @@ +{ + "version": 2, + "minor_version": 1, + "key": "homekit.v2.iids", + "data": { + "allocations": { + "1": { + "3E___": 1, + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "A2___": 8, + "A2__37_": 9 + }, + "935391877": { + "3E___": 1, + "3E__14_": 11, + "3E__20_": 12, + "3E__21_": 13, + "3E__23_": 14, + "3E__30_": 15, + "3E__52_": 16, + "4A___": 17, + "4A__F_": 18, + "4A__33_": 19, + "4A__11_": 20, + "4A__35_": 21, + "4A__36_": 22, + "4A__D_": 23, + "4A__12_": 24, + "4A__34_": 25, + "4A__10_": 26, + "B7___": 27, + "B7__B0_": 28, + "B7__BF_": 29, + "B7__AF_": 30 + }, + "985724734": { + "3E___": 1, + "3E__14_": 32, + "3E__20_": 33, + "3E__21_": 34, + "3E__23_": 35, + "3E__30_": 36, + "3E__52_": 37, + "4A___": 38, + "4A__F_": 39, + "4A__33_": 40, + "4A__11_": 41, + "4A__35_": 42, + "4A__36_": 43, + "4A__D_": 44, + "4A__12_": 45, + "4A__34_": 46, + "4A__10_": 47, + "B7___": 48, + "B7__B0_": 49, + "B7__BF_": 50, + "B7__AF_": 51 + }, + "3083074204": { + "3E___": 1, + "3E__14_": 53, + "3E__20_": 54, + "3E__21_": 55, + "3E__23_": 56, + "3E__30_": 57, + "3E__52_": 58, + "4A___": 59, + "4A__F_": 60, + "4A__33_": 61, + "4A__11_": 62, + "4A__35_": 63, + "4A__36_": 64, + "4A__D_": 65, + "4A__12_": 66, + "4A__34_": 67, + "4A__10_": 68, + "B7___": 69, + "B7__B0_": 70, + "B7__BF_": 71, + "B7__AF_": 72 + }, + "3032741347": { + "3E___": 1, + "3E__14_": 74, + "3E__20_": 75, + "3E__21_": 76, + "3E__23_": 77, + "3E__30_": 78, + "3E__52_": 79, + "4A___": 80, + "4A__F_": 81, + "4A__33_": 82, + "4A__11_": 83, + "4A__35_": 84, + "4A__36_": 85, + "4A__D_": 86, + "4A__12_": 87, + "4A__34_": 88, + "4A__10_": 89, + "B7___": 90, + "B7__B0_": 91, + "B7__BF_": 92, + "B7__AF_": 93 + }, + "3681509609": { + "3E___": 1, + "3E__14_": 95, + "3E__20_": 96, + "3E__21_": 97, + "3E__23_": 98, + "3E__30_": 99, + "3E__52_": 100, + "4A___": 101, + "4A__F_": 102, + "4A__33_": 103, + "4A__11_": 104, + "4A__35_": 105, + "4A__36_": 106, + "4A__D_": 107, + "4A__12_": 108, + "4A__34_": 109, + "4A__10_": 110, + "B7___": 111, + "B7__B0_": 112, + "B7__BF_": 113, + "B7__AF_": 114 + }, + "3866063418": { + "3E___": 1, + "3E__14_": 116, + "3E__20_": 117, + "3E__21_": 118, + "3E__23_": 119, + "3E__30_": 120, + "3E__52_": 121, + "4A___": 122, + "4A__F_": 123, + "4A__33_": 124, + "4A__11_": 125, + "4A__35_": 126, + "4A__36_": 127, + "4A__D_": 128, + "4A__12_": 129, + "4A__34_": 130, + "4A__10_": 131, + "B7___": 132, + "B7__B0_": 133, + "B7__BF_": 134, + "B7__AF_": 135 + }, + "3239498961": { + "3E___": 1, + "3E__14_": 137, + "3E__20_": 138, + "3E__21_": 139, + "3E__23_": 140, + "3E__30_": 141, + "3E__52_": 142, + "4A___": 143, + "4A__F_": 144, + "4A__33_": 145, + "4A__11_": 146, + "4A__35_": 147, + "4A__36_": 148, + "4A__D_": 149, + "4A__12_": 150, + "4A__34_": 151, + "4A__10_": 152, + "B7___": 153, + "B7__B0_": 154, + "B7__BF_": 155, + "B7__AF_": 156 + }, + "3289831818": { + "3E___": 1, + "3E__14_": 158, + "3E__20_": 159, + "3E__21_": 160, + "3E__23_": 161, + "3E__30_": 162, + "3E__52_": 163, + "4A___": 164, + "4A__F_": 165, + "4A__33_": 166, + "4A__11_": 167, + "4A__35_": 168, + "4A__36_": 169, + "4A__D_": 170, + "4A__12_": 171, + "4A__34_": 172, + "4A__10_": 173, + "B7___": 174, + "B7__B0_": 175, + "B7__BF_": 176, + "B7__AF_": 177 + }, + "3071722771": { + "3E___": 1, + "3E__14_": 179, + "3E__20_": 180, + "3E__21_": 181, + "3E__23_": 182, + "3E__30_": 183, + "3E__52_": 184, + "4A___": 185, + "4A__F_": 186, + "4A__33_": 187, + "4A__11_": 188, + "4A__35_": 189, + "4A__36_": 190, + "4A__D_": 191, + "4A__12_": 192, + "4A__34_": 193, + "4A__10_": 194, + "B7___": 195, + "B7__B0_": 196, + "B7__BF_": 197, + "B7__AF_": 198 + }, + "3391630365": { + "3E___": 1, + "3E__14_": 200, + "3E__20_": 201, + "3E__21_": 202, + "3E__23_": 203, + "3E__30_": 204, + "3E__52_": 205, + "4A___": 206, + "4A__F_": 207, + "4A__33_": 208, + "4A__11_": 209, + "4A__35_": 210, + "4A__36_": 211, + "4A__D_": 212, + "4A__12_": 213, + "4A__34_": 214, + "4A__10_": 215, + "B7___": 216, + "B7__B0_": 217, + "B7__BF_": 218, + "B7__AF_": 219 + }, + "3274187032": { + "3E___": 1, + "3E__14_": 221, + "3E__20_": 222, + "3E__21_": 223, + "3E__23_": 224, + "3E__30_": 225, + "3E__52_": 226, + "4A___": 227, + "4A__F_": 228, + "4A__33_": 229, + "4A__11_": 230, + "4A__35_": 231, + "4A__36_": 232, + "4A__D_": 233, + "4A__12_": 234, + "4A__34_": 235, + "4A__10_": 236, + "B7___": 237, + "B7__B0_": 238, + "B7__BF_": 239, + "B7__AF_": 240 + } + } + } +} diff --git a/tests/components/homekit/fixtures/iids_v2_with_underscore b/tests/components/homekit/fixtures/iids_v2_with_underscore new file mode 100644 index 00000000000000..52e874e41a5e9b --- /dev/null +++ b/tests/components/homekit/fixtures/iids_v2_with_underscore @@ -0,0 +1,54 @@ +{ + "version": 2, + "minor_version": 1, + "key": "homekit.8a47205bd97c07d7a908f10166ebe636.iids", + "data": { + "allocations": { + "1": { + "3E___": 1, + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "A2___": 8, + "A2__37_": 9 + }, + "1973560704": { + "3E___": 1, + "3E__14_": 11, + "3E__20_": 12, + "3E__21_": 13, + "3E__23_": 14, + "3E__30_": 15, + "3E__52_": 16, + "3E__53_": 17, + "89_pressed-__": 18, + "89_pressed-_73_": 19, + "89_pressed-_23_": 20, + "89_pressed-_CB_": 21, + "CC_pressed-__": 22, + "CC_pressed-_CD_": 23, + "89_changed_states-__": 24, + "89_changed_states-_73_": 25, + "89_changed_states-_23_": 26, + "89_changed_states-_CB_": 27, + "CC_changed_states-__": 28, + "CC_changed_states-_CD_": 29, + "89_turned_off-__": 30, + "89_turned_off-_73_": 31, + "89_turned_off-_23_": 32, + "89_turned_off-_CB_": 33, + "CC_turned_off-__": 34, + "CC_turned_off-_CD_": 35, + "89_turned_on-__": 36, + "89_turned_on-_73_": 37, + "89_turned_on-_23_": 38, + "89_turned_on-_CB_": 39, + "CC_turned_on-__": 40, + "CC_turned_on-_CD_": 41 + } + } + } +} \ No newline at end of file diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 2a0f3f2f718797..36eaeff91b496b 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -10,7 +10,6 @@ HomeAccessory, HomeBridge, HomeDriver, - HomeIIDManager, ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -724,7 +723,7 @@ def test_home_driver(iid_storage): "entry_id", "name", "title", - iid_manager=HomeIIDManager(iid_storage), + iid_storage=iid_storage, address=ip_address, port=port, persist_file=path, @@ -752,22 +751,3 @@ def test_home_driver(iid_storage): mock_unpair.assert_called_with("client_uuid") mock_show_msg.assert_called_with("hass", "entry_id", "title (any)", pin, "X-HM://0") - - -async def test_iid_collision_raises(hass, hk_driver): - """Test iid collision raises. - - If we try to allocate the same IID to the an accessory twice, we should - raise an exception. - """ - - entity_id = "light.accessory" - entity_id2 = "light.accessory2" - - hass.states.async_set(entity_id, STATE_OFF) - hass.states.async_set(entity_id2, STATE_OFF) - - HomeAccessory(hass, hk_driver, "Home Accessory", entity_id, 2, {}) - - with pytest.raises(RuntimeError): - HomeAccessory(hass, hk_driver, "Home Accessory", entity_id2, 2, {}) diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index 1f6f7c584f3ee6..be98c3bacdd77f 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -43,6 +43,19 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) assert diag == { "bridge": {}, + "iid_storage": { + "1": { + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "3E___": 1, + "A2__37_": 9, + "A2___": 8, + } + }, "accessories": [ { "aid": 1, @@ -257,6 +270,21 @@ async def test_config_entry_accessory( }, "config_version": 2, "pairing_id": ANY, + "iid_storage": { + "1": { + "3E__14_": 2, + "3E__20_": 3, + "3E__21_": 4, + "3E__23_": 5, + "3E__30_": 6, + "3E__52_": 7, + "3E___": 1, + "43__25_": 11, + "43___": 10, + "A2__37_": 9, + "A2___": 8, + } + }, "status": 1, } with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 21dc94a4b54371..d8c02aa98c414f 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -262,7 +262,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=zeroconf_mock, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) assert homekit.driver.safe_mode is False @@ -306,7 +306,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=mock_async_zeroconf, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) @@ -350,7 +350,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance=async_zeroconf_instance, zeroconf_server=f"{uuid}-hap.local.", loader=ANY, - iid_manager=ANY, + iid_storage=ANY, ) diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py index a791c30a3416e0..3e4a19c9045ad8 100644 --- a/tests/components/homekit/test_iidmanager.py +++ b/tests/components/homekit/test_iidmanager.py @@ -1,6 +1,5 @@ """Tests for the HomeKit IID manager.""" - from uuid import UUID from homeassistant.components.homekit.const import DOMAIN @@ -8,9 +7,10 @@ AccessoryIIDStorage, get_iid_storage_filename_for_entry_id, ) +from homeassistant.helpers.json import json_loads from homeassistant.util.uuid import random_uuid_hex -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture async def test_iid_generation_and_restore(hass, iid_storage, hass_storage): @@ -77,9 +77,6 @@ async def test_iid_generation_and_restore(hass, iid_storage, hass_storage): unique_service_unique_char_new_aid_iid1 == unique_service_unique_char_new_aid_iid2 ) - assert unique_service_unique_char_new_aid_iid1 != iid1 - assert unique_service_unique_char_new_aid_iid1 != unique_service_unique_char_iid1 - await iid_storage.async_save() iid_storage2 = AccessoryIIDStorage(hass, entry.entry_id) @@ -99,3 +96,79 @@ async def test_iid_storage_filename(hass, iid_storage, hass_storage): assert iid_storage.store.path.endswith( get_iid_storage_filename_for_entry_id(entry.entry_id) ) + + +async def test_iid_migration_to_v2(hass, iid_storage, hass_storage): + """Test iid storage migration.""" + v1_iids = json_loads(load_fixture("iids_v1", DOMAIN)) + v2_iids = json_loads(load_fixture("iids_v2", DOMAIN)) + hass_storage["homekit.v1.iids"] = v1_iids + hass_storage["homekit.v2.iids"] = v2_iids + + iid_storage_v2 = AccessoryIIDStorage(hass, "v1") + await iid_storage_v2.async_initialize() + + iid_storage_v1 = AccessoryIIDStorage(hass, "v2") + await iid_storage_v1.async_initialize() + + assert iid_storage_v1.allocations == iid_storage_v2.allocations + assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids + + assert len(iid_storage_v2.allocations) == 12 + + for allocations in iid_storage_v2.allocations.values(): + assert allocations["3E___"] == 1 + + +async def test_iid_migration_to_v2_with_underscore(hass, iid_storage, hass_storage): + """Test iid storage migration with underscore.""" + v1_iids = json_loads(load_fixture("iids_v1_with_underscore", DOMAIN)) + v2_iids = json_loads(load_fixture("iids_v2_with_underscore", DOMAIN)) + hass_storage["homekit.v1_with_underscore.iids"] = v1_iids + hass_storage["homekit.v2_with_underscore.iids"] = v2_iids + + iid_storage_v2 = AccessoryIIDStorage(hass, "v1_with_underscore") + await iid_storage_v2.async_initialize() + + iid_storage_v1 = AccessoryIIDStorage(hass, "v2_with_underscore") + await iid_storage_v1.async_initialize() + + assert iid_storage_v1.allocations == iid_storage_v2.allocations + assert iid_storage_v1.allocated_iids == iid_storage_v2.allocated_iids + + assert len(iid_storage_v2.allocations) == 2 + + for allocations in iid_storage_v2.allocations.values(): + assert allocations["3E___"] == 1 + + +async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage): + """Test generating iids and restoring them from storage.""" + entry = MockConfigEntry(domain=DOMAIN) + + iid_storage = AccessoryIIDStorage(hass, entry.entry_id) + await iid_storage.async_initialize() + not_accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "000000AA-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid == 2 + not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( + 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid_2 == 3 + not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( + 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None + ) + assert not_accessory_info_service_iid_2 == 3 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 + accessory_info_service_iid = iid_storage.get_or_allocate_iid( + 2, "0000003E-0000-1000-8000-0026BB765291", None, None, None + ) + assert accessory_info_service_iid == 1 From c585817e6719ae019468924f88580920ccbfdc05 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 11:18:58 +0100 Subject: [PATCH 306/394] Adjust REST schema validation (#81723) fixes undefined --- homeassistant/components/rest/schema.py | 9 ++++++++- tests/components/rest/test_init.py | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index f881dc8b028647..d124ce5789c9e8 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -89,6 +89,13 @@ ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])}, + { + DOMAIN: vol.All( + # convert empty dict to empty list + lambda x: [] if x == {} else x, + cv.ensure_list, + [COMBINED_SCHEMA], + ) + }, extra=vol.ALLOW_EXTRA, ) diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 6dd2650c25c2c9..08d538fd163e6c 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -19,7 +19,11 @@ from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, get_fixture_path +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + get_fixture_path, +) @respx.mock @@ -400,3 +404,17 @@ async def test_multiple_rest_endpoints(hass: HomeAssistant) -> None: assert hass.states.get("sensor.json_date_time").state == "07:11:08 PM" assert hass.states.get("sensor.json_time").state == "07:11:39 PM" assert hass.states.get("binary_sensor.binary_sensor").state == "on" + + +async def test_empty_config(hass: HomeAssistant) -> None: + """Test setup with empty configuration. + + For example (with rest.yaml an empty file): + rest: !include rest.yaml + """ + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {}}, + ) + assert_setup_component(0, DOMAIN) From 4d1fa42a3c96e098e5c08930f8706313e3484e23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 04:20:54 -0600 Subject: [PATCH 307/394] Use more efficient async_progress_by_handler call in async_start_reauth (#81757) --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b7ee9a4d6549db..bd985517ca74d6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -662,7 +662,7 @@ def async_start_reauth( """Start a reauth flow.""" if any( flow - for flow in hass.config_entries.flow.async_progress() + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain) if flow["context"].get("source") == SOURCE_REAUTH and flow["context"].get("entry_id") == self.entry_id ): From dfab3b26514a4c600e108c11ff1e52534ea0b51c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 8 Nov 2022 02:21:13 -0800 Subject: [PATCH 308/394] Partially revert google local sync for search cases (#81761) fixes undefined --- homeassistant/components/google/calendar.py | 154 +++++++++++++++----- 1 file changed, 118 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ca1228759cde8e..4eb57cff49c114 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -3,11 +3,12 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from datetime import datetime, timedelta import logging from typing import Any -from gcal_sync.api import SyncEventsRequest +from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest from gcal_sync.exceptions import ApiException from gcal_sync.model import DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore @@ -196,21 +197,30 @@ async def async_setup_entry( entity_registry.async_remove( entity_entry.entity_id, ) - request_template = SyncEventsRequest( - calendar_id=calendar_id, - search=data.get(CONF_SEARCH), - start_time=dt_util.now() + SYNC_EVENT_MIN_TIME, - ) - sync = CalendarEventSyncManager( - calendar_service, - store=ScopedCalendarStore(store, unique_id or entity_name), - request_template=request_template, - ) - coordinator = CalendarUpdateCoordinator( - hass, - sync, - data[CONF_NAME], - ) + coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator + if search := data.get(CONF_SEARCH): + coordinator = CalendarQueryUpdateCoordinator( + hass, + calendar_service, + data[CONF_NAME], + calendar_id, + search, + ) + else: + request_template = SyncEventsRequest( + calendar_id=calendar_id, + start_time=dt_util.now() + SYNC_EVENT_MIN_TIME, + ) + sync = CalendarEventSyncManager( + calendar_service, + store=ScopedCalendarStore(store, unique_id or entity_name), + request_template=request_template, + ) + coordinator = CalendarSyncUpdateCoordinator( + hass, + sync, + data[CONF_NAME], + ) entities.append( GoogleCalendarEntity( coordinator, @@ -242,8 +252,8 @@ def append_calendars_to_config() -> None: ) -class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]): - """Coordinator for calendar RPC calls.""" +class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]): + """Coordinator for calendar RPC calls that use an efficient sync.""" def __init__( self, @@ -251,7 +261,7 @@ def __init__( sync: CalendarEventSyncManager, name: str, ) -> None: - """Create the Calendar event device.""" + """Create the CalendarSyncUpdateCoordinator.""" super().__init__( hass, _LOGGER, @@ -271,6 +281,87 @@ async def _async_update_data(self) -> Timeline: dt_util.DEFAULT_TIME_ZONE ) + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> Iterable[Event]: + """Get all events in a specific time frame.""" + if not self.data: + raise HomeAssistantError( + "Unable to get events: Sync from server has not completed" + ) + return self.data.overlapping( + dt_util.as_local(start_date), + dt_util.as_local(end_date), + ) + + @property + def upcoming(self) -> Iterable[Event] | None: + """Return upcoming events if any.""" + if self.data: + return self.data.active_after(dt_util.now()) + return None + + +class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): + """Coordinator for calendar RPC calls. + + This sends a polling RPC, not using sync, as a workaround + for limitations in the calendar API for supporting search. + """ + + def __init__( + self, + hass: HomeAssistant, + calendar_service: GoogleCalendarService, + name: str, + calendar_id: str, + search: str | None, + ) -> None: + """Create the CalendarQueryUpdateCoordinator.""" + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self._search = search + + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> Iterable[Event]: + """Get all events in a specific time frame.""" + request = ListEventsRequest( + calendar_id=self.calendar_id, + start_time=start_date, + end_time=end_date, + search=self._search, + ) + result_items = [] + try: + result = await self.calendar_service.async_list_events(request) + async for result_page in result: + result_items.extend(result_page.items) + except ApiException as err: + self.async_set_update_error(err) + raise HomeAssistantError(str(err)) from err + return result_items + + async def _async_update_data(self) -> list[Event]: + """Fetch data from API endpoint.""" + request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) + try: + result = await self.calendar_service.async_list_events(request) + except ApiException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return result.items + + @property + def upcoming(self) -> Iterable[Event] | None: + """Return the next upcoming event if any.""" + return self.data + class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): """A calendar event entity.""" @@ -279,7 +370,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): def __init__( self, - coordinator: CalendarUpdateCoordinator, + coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator, calendar_id: str, data: dict[str, Any], entity_id: str, @@ -352,14 +443,7 @@ async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - if not (timeline := self.coordinator.data): - raise HomeAssistantError( - "Unable to get events: Sync from server has not completed" - ) - result_items = timeline.overlapping( - dt_util.as_local(start_date), - dt_util.as_local(end_date), - ) + result_items = await self.coordinator.async_get_events(start_date, end_date) return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) @@ -367,14 +451,12 @@ async def async_get_events( def _apply_coordinator_update(self) -> None: """Copy state from the coordinator to this entity.""" - if (timeline := self.coordinator.data) and ( - api_event := next( - filter( - self._event_filter, - timeline.active_after(dt_util.now()), - ), - None, - ) + if api_event := next( + filter( + self._event_filter, + self.coordinator.upcoming or [], + ), + None, ): self._event = _get_calendar_event(api_event) (self._event.summary, self._offset_value) = extract_offset( From d6c10cd887000d80bc9a220a2096f74e229488f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 8 Nov 2022 12:28:36 +0100 Subject: [PATCH 309/394] Bump pycfdns from 1.2.2 to 2.0.0 (#81776) --- homeassistant/components/cloudflare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index 73b83c24cce2db..55e43eb4fdeff1 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -2,7 +2,7 @@ "domain": "cloudflare", "name": "Cloudflare", "documentation": "https://www.home-assistant.io/integrations/cloudflare", - "requirements": ["pycfdns==1.2.2"], + "requirements": ["pycfdns==2.0.0"], "codeowners": ["@ludeeus", "@ctalkington"], "config_flow": true, "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index d8243dad9c6010..71c41035267cad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1487,7 +1487,7 @@ pybravia==0.2.3 pycarwings2==2.13 # homeassistant.components.cloudflare -pycfdns==1.2.2 +pycfdns==2.0.0 # homeassistant.components.channels pychannels==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9270aa1a925921..0fa8d9274cd81e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1063,7 +1063,7 @@ pybotvac==0.0.23 pybravia==0.2.3 # homeassistant.components.cloudflare -pycfdns==1.2.2 +pycfdns==2.0.0 # homeassistant.components.cast pychromecast==12.1.4 From 47dba6f6bc7ab1a1877b53818120f1698952c1fe Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 12:55:41 +0100 Subject: [PATCH 310/394] Improve MQTT type hints part 5 (#80979) * Improve typing scene * Improve typing select * Improve typing sensor * move expire_after - and class level attrs * Follow up comment * Solve type confict * Remove stale sentinel const * Update homeassistant/components/mqtt/sensor.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Make _expire_after a class attribute * Code styling Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/mqtt/scene.py | 20 ++++-- homeassistant/components/mqtt/select.py | 74 +++++++++++---------- homeassistant/components/mqtt/sensor.py | 87 +++++++++++++++---------- 3 files changed, 106 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index e237d70e903021..9eafd0cdd990f3 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -88,8 +88,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT scene.""" async_add_entities([MqttScene(hass, config, config_entry, discovery_data)]) @@ -103,23 +103,29 @@ class MqttScene( _entity_id_format = scene.DOMAIN + ".{}" - def __init__(self, hass, config, config_entry, discovery_data): + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT scene.""" MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._config = config - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" async def async_activate(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 12593550e2fbb1..6dfe5081e74c56 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -1,6 +1,7 @@ """Configure select in a device through MQTT topic.""" from __future__ import annotations +from collections.abc import Callable import functools import logging @@ -34,7 +35,13 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data _LOGGER = logging.getLogger(__name__) @@ -103,8 +110,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT select.""" async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)]) @@ -114,53 +121,55 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): """representation of an MQTT select.""" _entity_id_format = select.ENTITY_ID_FORMAT - _attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED - - def __init__(self, hass, config, config_entry, discovery_data): + _command_template: Callable[[PublishPayloadType], PublishPayloadType] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + _optimistic: bool = False + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT select.""" - self._config = config - self._optimistic = False - self._sub_state = None - - self._attr_current_option = None - SelectEntity.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" + self._attr_current_option = None self._optimistic = config[CONF_OPTIMISTIC] self._attr_options = config[CONF_OPTIONS] - self._templates = { - CONF_COMMAND_TEMPLATE: MqttCommandTemplate( - config.get(CONF_COMMAND_TEMPLATE), entity=self - ).async_render, - CONF_VALUE_TEMPLATE: MqttValueTemplate( - config.get(CONF_VALUE_TEMPLATE), - entity=self, - ).async_render_with_possible_json_value, - } - - def _prepare_subscribe_topics(self): + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), + entity=self, + ).async_render + self._value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), entity=self + ).async_render_with_possible_json_value + + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" - payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload) - + payload = str(self._value_template(msg.payload)) if payload.lower() == "none": - payload = None + self._attr_current_option = None + get_mqtt_data(self.hass).state_write_requests.write_state_request(self) + return - if payload is not None and payload not in self.options: + if payload not in self.options: _LOGGER.error( "Invalid option for %s: '%s' (valid options: %s)", self.entity_id, @@ -168,7 +177,6 @@ def message_received(msg): self.options, ) return - self._attr_current_option = payload get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -189,7 +197,7 @@ def message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -198,7 +206,7 @@ async def _subscribe_topics(self): async def async_select_option(self, option: str) -> None: """Update the current value.""" - payload = self._templates[CONF_COMMAND_TEMPLATE](option) + payload = self._command_template(option) if self._optimistic: self._attr_current_option = option self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4c6b5409962fb2..ed65b5a42fe54d 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,9 +1,11 @@ """Support for MQTT sensors.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import functools import logging +from typing import Any import voluptuous as vol @@ -15,6 +17,7 @@ STATE_CLASSES_SCHEMA, RestoreSensor, SensorDeviceClass, + SensorExtraStoredData, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,7 +29,7 @@ STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time @@ -45,7 +48,12 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate, PayloadSentinel, ReceiveMessage +from .models import ( + MqttValueTemplate, + PayloadSentinel, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -65,7 +73,7 @@ DEFAULT_FORCE_UPDATE = False -def validate_options(conf): +def validate_options(conf: ConfigType) -> ConfigType: """Validate options. If last reset topic is present it must be same as the state topic. @@ -155,8 +163,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT sensor.""" async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) @@ -168,24 +176,29 @@ class MqttSensor(MqttEntity, RestoreSensor): _entity_id_format = ENTITY_ID_FORMAT _attr_last_reset = None _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED - - def __init__(self, hass, config, config_entry, discovery_data): + _expire_after: int | None + _expired: bool | None + _template: Callable[[ReceivePayloadType, PayloadSentinel], ReceivePayloadType] + _last_reset_template: Callable[[ReceivePayloadType], ReceivePayloadType] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the sensor.""" - self._expiration_trigger = None - - expire_after = config.get(CONF_EXPIRE_AFTER) - if expire_after is not None and expire_after > 0: - self._expired = True - else: - self._expired = None - + self._expiration_trigger: CALLBACK_TYPE | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) async def mqtt_async_added_to_hass(self) -> None: """Restore state for entities with expire_after set.""" + last_state: State | None + last_sensor_data: SensorExtraStoredData | None if ( - (expire_after := self._config.get(CONF_EXPIRE_AFTER)) is not None - and expire_after > 0 + (_expire_after := self._expire_after) is not None + and _expire_after > 0 and (last_state := await self.async_get_last_state()) is not None and last_state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE] and (last_sensor_data := await self.async_get_last_sensor_data()) @@ -194,7 +207,7 @@ async def mqtt_async_added_to_hass(self) -> None: # MqttEntity.async_added_to_hass(), then we should not restore state and not self._expiration_trigger ): - expiration_at = last_state.last_changed + timedelta(seconds=expire_after) + expiration_at = last_state.last_changed + timedelta(seconds=_expire_after) if expiration_at < (time_now := dt_util.utcnow()): # Skip reactivating the sensor _LOGGER.debug("Skip state recovery after reload for %s", self.entity_id) @@ -222,7 +235,7 @@ async def async_will_remove_from_hass(self) -> None: await MqttEntity.async_will_remove_from_hass(self) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA @@ -233,6 +246,12 @@ def _setup_from_config(self, config: ConfigType) -> None: self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = config.get(CONF_STATE_CLASS) + self._expire_after = config.get(CONF_EXPIRE_AFTER) + if self._expire_after is not None and self._expire_after > 0: + self._expired = True + else: + self._expired = None + self._template = MqttValueTemplate( self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value @@ -240,15 +259,14 @@ def _setup_from_config(self, config: ConfigType) -> None: self._config.get(CONF_LAST_RESET_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" - topics = {} + topics: dict[str, dict[str, Any]] = {} def _update_state(msg: ReceiveMessage) -> None: # auto-expire enabled? - expire_after = self._config.get(CONF_EXPIRE_AFTER) - if expire_after is not None and expire_after > 0: - # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + 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 self._expired = False # Reset old trigger @@ -256,13 +274,13 @@ def _update_state(msg: ReceiveMessage) -> None: self._expiration_trigger() # Set new trigger - expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) + expiration_at = dt_util.utcnow() + timedelta(seconds=self._expire_after) self._expiration_trigger = async_track_point_in_utc_time( self.hass, self._value_is_expired, expiration_at ) - payload = self._template(msg.payload, default=PayloadSentinel.DEFAULT) + payload = self._template(msg.payload, PayloadSentinel.DEFAULT) if payload is PayloadSentinel.DEFAULT: return if self.device_class not in { @@ -282,14 +300,14 @@ def _update_state(msg: ReceiveMessage) -> None: return self._attr_native_value = payload_datetime - def _update_last_reset(msg): + def _update_last_reset(msg: ReceiveMessage) -> None: payload = self._last_reset_template(msg.payload) if not payload: _LOGGER.debug("Ignoring empty last_reset message from '%s'", msg.topic) return try: - last_reset = dt_util.parse_datetime(payload) + last_reset = dt_util.parse_datetime(str(payload)) if last_reset is None: raise ValueError self._attr_last_reset = last_reset @@ -300,7 +318,7 @@ def _update_last_reset(msg): @callback @log_messages(self.hass, self.entity_id) - def message_received(msg): + def message_received(msg: ReceiveMessage) -> None: """Handle new MQTT messages.""" _update_state(msg) if CONF_LAST_RESET_VALUE_TEMPLATE in self._config and ( @@ -319,7 +337,7 @@ def message_received(msg): @callback @log_messages(self.hass, self.entity_id) - def last_reset_message_received(msg): + def last_reset_message_received(msg: ReceiveMessage) -> None: """Handle new last_reset messages.""" _update_last_reset(msg) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -339,12 +357,12 @@ def last_reset_message_received(msg): self.hass, self._sub_state, topics ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @callback - def _value_is_expired(self, *_): + def _value_is_expired(self, *_: datetime) -> None: """Triggered when value is expired.""" self._expiration_trigger = None self._expired = True @@ -353,8 +371,7 @@ def _value_is_expired(self, *_): @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" - expire_after = self._config.get(CONF_EXPIRE_AFTER) # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 return MqttAvailability.available.fget(self) and ( # type: ignore[attr-defined] - expire_after is None or not self._expired + self._expire_after is None or not self._expired ) From 4293c88fb664f9fce73974da4a279decd450dd07 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 13:11:45 +0100 Subject: [PATCH 311/394] Improve MQTT type hints part 6 (#81001) * Improve typing siren * Improve typing switch * Set siren type hints at class level * Set switch type hints at class level * Follow up comment * Improve hints on siren templates * Another cleanup for siren * Follow up comment * Follow up comment --- homeassistant/components/mqtt/siren.py | 109 +++++++++++++----------- homeassistant/components/mqtt/switch.py | 36 +++++--- 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 2ab226e44c0d3a..4a69977df45e2e 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -1,10 +1,11 @@ """Support for MQTT sirens.""" from __future__ import annotations +from collections.abc import Callable import copy import functools import logging -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -17,6 +18,7 @@ TURN_ON_SCHEMA, SirenEntity, SirenEntityFeature, + SirenTurnOnServiceParameters, process_turn_on_params, ) from homeassistant.config_entries import ConfigEntry @@ -30,7 +32,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType from . import subscription from .config import MQTT_RW_SCHEMA @@ -53,7 +56,13 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttCommandTemplate, MqttValueTemplate +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" @@ -150,8 +159,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT siren.""" async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)]) @@ -162,29 +171,32 @@ class MqttSiren(MqttEntity, SirenEntity): _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SIREN_ATTRIBUTES_BLOCKED + _attr_supported_features: int - def __init__(self, hass, config, config_entry, discovery_data): - """Initialize the MQTT siren.""" - self._attr_name = config[CONF_NAME] - self._attr_should_poll = False - self._supported_features = SUPPORTED_BASE - self._attr_is_on = None - self._state_on = None - self._state_off = None - self._optimistic = None - - self._attr_extra_state_attributes: dict[str, Any] = {} + _command_templates: dict[ + str, Callable[[PublishPayloadType, TemplateVarsType], PublishPayloadType] | None + ] + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + _state_on: str + _state_off: str + _optimistic: bool - self.target = None - - super().__init__(hass, config, config_entry, discovery_data) + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: + """Initialize the MQTT siren.""" + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA - def _setup_from_config(self, config): + def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" state_on = config.get(CONF_STATE_ON) @@ -193,25 +205,29 @@ def _setup_from_config(self, config): state_off = config.get(CONF_STATE_OFF) self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] + self._attr_extra_state_attributes = {} + + _supported_features: int = SUPPORTED_BASE if config[CONF_SUPPORT_DURATION]: - self._supported_features |= SirenEntityFeature.DURATION + _supported_features |= SirenEntityFeature.DURATION self._attr_extra_state_attributes[ATTR_DURATION] = None if config.get(CONF_AVAILABLE_TONES): - self._supported_features |= SirenEntityFeature.TONES + _supported_features |= SirenEntityFeature.TONES self._attr_available_tones = config[CONF_AVAILABLE_TONES] self._attr_extra_state_attributes[ATTR_TONE] = None if config[CONF_SUPPORT_VOLUME_SET]: - self._supported_features |= SirenEntityFeature.VOLUME_SET + _supported_features |= SirenEntityFeature.VOLUME_SET self._attr_extra_state_attributes[ATTR_VOLUME_LEVEL] = None + self._attr_supported_features = _supported_features self._optimistic = config[CONF_OPTIMISTIC] or CONF_STATE_TOPIC not in config self._attr_is_on = False if self._optimistic else None - command_template = config.get(CONF_COMMAND_TEMPLATE) - command_off_template = config.get(CONF_COMMAND_OFF_TEMPLATE) or config.get( - CONF_COMMAND_TEMPLATE + command_template: Template | None = config.get(CONF_COMMAND_TEMPLATE) + command_off_template: Template | None = ( + config.get(CONF_COMMAND_OFF_TEMPLATE) or command_template ) self._command_templates = { CONF_COMMAND_TEMPLATE: MqttCommandTemplate( @@ -230,12 +246,12 @@ def _setup_from_config(self, config): entity=self, ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT state messages.""" payload = self._value_template(msg.payload) if not payload or payload == PAYLOAD_EMPTY_JSON: @@ -245,7 +261,7 @@ def state_message_received(msg): msg.topic, ) return - json_payload = {} + json_payload: dict[str, Any] = {} if payload in [self._state_on, self._state_off, PAYLOAD_NONE]: json_payload = {STATE: payload} else: @@ -275,7 +291,8 @@ def state_message_received(msg): if json_payload: # process attributes try: - vol.All(TURN_ON_SCHEMA)(json_payload) + params: SirenTurnOnServiceParameters + params = vol.All(TURN_ON_SCHEMA)(json_payload) except vol.MultipleInvalid as invalid_siren_parameters: _LOGGER.warning( "Unable to update siren state attributes from payload '%s': %s", @@ -283,7 +300,7 @@ def state_message_received(msg): invalid_siren_parameters, ) return - self._update(process_turn_on_params(self, json_payload)) + self._update(process_turn_on_params(self, params)) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC) is None: @@ -303,7 +320,7 @@ def state_message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) @@ -322,11 +339,6 @@ def extra_state_attributes(self) -> dict[str, Any]: attributes.update(self._attr_extra_state_attributes) return attributes - @property - def supported_features(self) -> int: - """Flag supported features.""" - return self._supported_features - async def _async_publish( self, topic: str, @@ -335,15 +347,14 @@ async def _async_publish( variables: dict[str, Any] | None = None, ) -> None: """Publish MQTT payload with optional command template.""" - template_variables = {STATE: value} + template_variables: dict[str, Any] = {STATE: value} if variables is not None: template_variables.update(variables) - payload = ( - self._command_templates[template](value, template_variables) - if self._command_templates[template] - else json_dumps(template_variables) - ) - if payload and payload not in PAYLOAD_NONE: + if command_template := self._command_templates[template]: + payload = command_template(value, template_variables) + else: + payload = json_dumps(template_variables) + if payload and str(payload) != PAYLOAD_NONE: await self.async_publish( self._config[topic], payload, @@ -367,7 +378,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: # Optimistically assume that siren has changed state. _LOGGER.debug("Writing state attributes %s", kwargs) self._attr_is_on = True - self._update(kwargs) + self._update(cast(SirenTurnOnServiceParameters, kwargs)) self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: @@ -386,8 +397,8 @@ async def async_turn_off(self, **kwargs: Any) -> None: self._attr_is_on = False self.async_write_ha_state() - def _update(self, data: dict[str, Any]) -> None: + def _update(self, data: SirenTurnOnServiceParameters) -> None: """Update the extra siren state attributes.""" for attribute, support in SUPPORTED_ATTRIBUTES.items(): - if self._supported_features & support and attribute in data: - self._attr_extra_state_attributes[attribute] = data[attribute] + if self._attr_supported_features & support and attribute in data: + 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 f2a40facd8b411..a20603e2399a51 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,6 +1,7 @@ """Support for MQTT switches.""" from __future__ import annotations +from collections.abc import Callable import functools from typing import Any @@ -22,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -42,7 +44,7 @@ async_setup_platform_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate +from .models import MqttValueTemplate, ReceiveMessage from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" @@ -107,8 +109,8 @@ async def _async_setup_entity( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, config: ConfigType, - config_entry: ConfigEntry | None = None, - discovery_data: dict | None = None, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None = None, ) -> None: """Set up the MQTT switch.""" async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) @@ -119,16 +121,24 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): _entity_id_format = switch.ENTITY_ID_FORMAT - def __init__(self, hass, config, config_entry, discovery_data): + _optimistic: bool + _state_on: str + _state_off: str + _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + config_entry: ConfigEntry, + discovery_data: DiscoveryInfoType | None, + ) -> None: """Initialize the MQTT switch.""" - self._state_on = None - self._state_off = None - self._optimistic = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod - def config_schema(): + def config_schema() -> vol.Schema: """Return the config schema.""" return DISCOVERY_SCHEMA @@ -136,10 +146,10 @@ def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" self._attr_device_class = config.get(CONF_DEVICE_CLASS) - state_on = config.get(CONF_STATE_ON) + state_on: str | None = config.get(CONF_STATE_ON) self._state_on = state_on if state_on else config[CONF_PAYLOAD_ON] - state_off = config.get(CONF_STATE_OFF) + state_off: str | None = config.get(CONF_STATE_OFF) self._state_off = state_off if state_off else config[CONF_PAYLOAD_OFF] self._optimistic = ( @@ -150,12 +160,12 @@ def _setup_from_config(self, config: ConfigType) -> None: self._config.get(CONF_VALUE_TEMPLATE), entity=self ).async_render_with_possible_json_value - def _prepare_subscribe_topics(self): + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @callback @log_messages(self.hass, self.entity_id) - def state_message_received(msg): + def state_message_received(msg: ReceiveMessage) -> None: """Handle new MQTT state messages.""" payload = self._value_template(msg.payload) if payload == self._state_on: @@ -184,7 +194,7 @@ def state_message_received(msg): }, ) - async def _subscribe_topics(self): + async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) From 9b0b8ae9c07ce3b1c04a2c498eb255415793c729 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:58:47 +0100 Subject: [PATCH 312/394] Add short-hand attributes to vacuum (#81782) Add attributes to vacuum --- homeassistant/components/vacuum/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 2942078a8755a0..4488f8062d6ead 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -318,11 +318,12 @@ class VacuumEntity(_BaseVacuum, ToggleEntity): """Representation of a vacuum cleaner robot.""" entity_description: VacuumEntityDescription + _attr_status: str | None = None @property def status(self) -> str | None: """Return the status of the vacuum cleaner.""" - return None + return self._attr_status @property def battery_icon(self) -> str: @@ -394,11 +395,12 @@ class StateVacuumEntity(_BaseVacuum): """Representation of a vacuum cleaner robot that supports states.""" entity_description: StateVacuumEntityDescription + _attr_state: str | None = None @property def state(self) -> str | None: """Return the state of the vacuum cleaner.""" - return None + return self._attr_state @property def battery_icon(self) -> str: From b7533aff486e6902feda1febf30f775f6fbb8ec2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 07:26:16 -0600 Subject: [PATCH 313/394] Replace led-ble util with bluetooth-data-tools (#81093) --- .coveragerc | 1 - .../components/bluetooth/manifest.json | 1 + .../components/led_ble/config_flow.py | 2 +- .../components/led_ble/manifest.json | 2 +- homeassistant/components/led_ble/util.py | 51 ------------------- homeassistant/package_constraints.txt | 1 + requirements_all.txt | 4 ++ requirements_test_all.txt | 4 ++ 8 files changed, 12 insertions(+), 54 deletions(-) delete mode 100644 homeassistant/components/led_ble/util.py diff --git a/.coveragerc b/.coveragerc index 84f061fbd35b16..1a8bfb462d8d84 100644 --- a/.coveragerc +++ b/.coveragerc @@ -678,7 +678,6 @@ omit = homeassistant/components/lcn/services.py homeassistant/components/led_ble/__init__.py homeassistant/components/led_ble/light.py - homeassistant/components/led_ble/util.py homeassistant/components/lg_netcast/media_player.py homeassistant/components/lg_soundbar/media_player.py homeassistant/components/lidarr/__init__.py diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 923ab248a2da44..cc7237d458596d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -10,6 +10,7 @@ "bleak-retry-connector==2.8.3", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", + "bluetooth-data-tools==0.2.0", "dbus-fast==1.72.0" ], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/led_ble/config_flow.py b/homeassistant/components/led_ble/config_flow.py index 19be92f6647376..d757b5021af124 100644 --- a/homeassistant/components/led_ble/config_flow.py +++ b/homeassistant/components/led_ble/config_flow.py @@ -4,6 +4,7 @@ import logging from typing import Any +from bluetooth_data_tools import human_readable_name from led_ble import BLEAK_EXCEPTIONS, LEDBLE import voluptuous as vol @@ -16,7 +17,6 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, LOCAL_NAMES, UNSUPPORTED_SUB_MODEL -from .util import human_readable_name _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 6802eea9bc7509..d59570c6257f65 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -3,7 +3,7 @@ "name": "LED BLE", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/led_ble/", - "requirements": ["led-ble==1.0.0"], + "requirements": ["bluetooth-data-tools==0.2.0", "led-ble==1.0.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [ diff --git a/homeassistant/components/led_ble/util.py b/homeassistant/components/led_ble/util.py deleted file mode 100644 index e43655e29058d7..00000000000000 --- a/homeassistant/components/led_ble/util.py +++ /dev/null @@ -1,51 +0,0 @@ -"""The yalexs_ble integration models.""" -from __future__ import annotations - -from homeassistant.components.bluetooth import ( - BluetoothScanningMode, - BluetoothServiceInfoBleak, - async_discovered_service_info, - async_process_advertisements, -) -from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher -from homeassistant.core import HomeAssistant, callback - -from .const import DEVICE_TIMEOUT - - -@callback -def async_find_existing_service_info( - hass: HomeAssistant, local_name: str, address: str -) -> BluetoothServiceInfoBleak | None: - """Return the service info for the given local_name and address.""" - for service_info in async_discovered_service_info(hass): - device = service_info.device - if device.address == address: - return service_info - return None - - -async def async_get_service_info( - hass: HomeAssistant, local_name: str, address: str -) -> BluetoothServiceInfoBleak: - """Wait for the service info for the given local_name and address.""" - if service_info := async_find_existing_service_info(hass, local_name, address): - return service_info - return await async_process_advertisements( - hass, - lambda service_info: True, - BluetoothCallbackMatcher({ADDRESS: address}), - BluetoothScanningMode.ACTIVE, - DEVICE_TIMEOUT, - ) - - -def short_address(address: str) -> str: - """Convert a Bluetooth address to a short address.""" - split_address = address.replace("-", ":").split(":") - return f"{split_address[-2].upper()}{split_address[-1].upper()}"[-4:] - - -def human_readable_name(name: str | None, local_name: str, address: str) -> str: - """Return a human readable name for the given name, local_name, and address.""" - return f"{name or local_name} ({short_address(address)})" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2aa2179a890751..4a6b8b076f7abe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,6 +14,7 @@ bleak-retry-connector==2.8.3 bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 +bluetooth-data-tools==0.2.0 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 71c41035267cad..512c366f5dc300 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -452,6 +452,10 @@ bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 +# homeassistant.components.bluetooth +# homeassistant.components.led_ble +bluetooth-data-tools==0.2.0 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fa8d9274cd81e..bc44c0c0b6b402 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -366,6 +366,10 @@ bluetooth-adapters==0.7.0 # homeassistant.components.bluetooth bluetooth-auto-recovery==0.3.6 +# homeassistant.components.bluetooth +# homeassistant.components.led_ble +bluetooth-data-tools==0.2.0 + # homeassistant.components.bond bond-async==0.1.22 From 2082026ff5240d89855ca63355043e0e12ef3e98 Mon Sep 17 00:00:00 2001 From: chpego <38792705+chpego@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:39:53 +0000 Subject: [PATCH 314/394] Fix Fully Kiosk start application service field (#81738) Fix attributes services to start_application --- homeassistant/components/fully_kiosk/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fully_kiosk/services.yaml b/homeassistant/components/fully_kiosk/services.yaml index b8ea6b371d7d7f..88178e35809a0d 100644 --- a/homeassistant/components/fully_kiosk/services.yaml +++ b/homeassistant/components/fully_kiosk/services.yaml @@ -20,7 +20,7 @@ start_application: device: integration: fully_kiosk fields: - url: + application: name: Application description: Package name of the application to start. example: "de.ozerov.fully" From 0c8eeaa6436b04ba6da46bccab8b11523f314d9b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 8 Nov 2022 14:41:39 +0100 Subject: [PATCH 315/394] Update mypy to 0.990 (#81783) * Update mypy to 0.990 * Remove type ignore - overriding attr with property (13475) * Remove type ignores - hasattr (13544) * Adjust type ignore - assignment (13549) * New error code - type-abstract (13785) * Disable annotation-unchecked (13851) --- homeassistant/auth/__init__.py | 3 +-- homeassistant/auth/mfa_modules/__init__.py | 1 - homeassistant/auth/providers/__init__.py | 4 +--- homeassistant/components/climate/__init__.py | 4 ++-- homeassistant/components/knx/__init__.py | 4 ++-- homeassistant/components/knx/schema.py | 4 ++-- .../components/media_player/__init__.py | 16 ++++------------ homeassistant/components/notify/legacy.py | 2 +- homeassistant/components/recorder/models.py | 6 +++--- homeassistant/helpers/entity.py | 4 ++-- homeassistant/util/yaml/dumper.py | 4 +++- homeassistant/util/yaml/loader.py | 2 +- mypy.ini | 1 + requirements_test.txt | 2 +- script/hassfest/mypy_config.py | 3 ++- 15 files changed, 26 insertions(+), 34 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 12511a7f4a5ed7..bbd23983e2bad6 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -356,8 +356,7 @@ async def async_remove_credentials(self, credentials: models.Credentials) -> Non provider = self._async_get_auth_provider(credentials) if provider is not None and hasattr(provider, "async_will_remove_credentials"): - # https://github.com/python/mypy/issues/1424 - await provider.async_will_remove_credentials(credentials) # type: ignore[attr-defined] + await provider.async_will_remove_credentials(credentials) await self._store.async_remove_credentials(credentials) diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 61c36da6e90320..ebfe1332cd2e6c 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -166,7 +166,6 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul processed = hass.data[DATA_REQS] = set() - # https://github.com/python/mypy/issues/1424 await requirements.async_process_requirements( hass, module_path, module.REQUIREMENTS ) diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 6feb4b267594fc..2448225a284ad6 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -250,9 +250,7 @@ async def async_step_mfa( auth_module, "async_initialize_login_mfa_step" ): try: - await auth_module.async_initialize_login_mfa_step( # type: ignore[attr-defined] - self.user.id - ) + await auth_module.async_initialize_login_mfa_step(self.user.id) except HomeAssistantError: _LOGGER.exception("Error initializing MFA step") return self.async_abort(reason="unknown_error") diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 67348d38625152..be5f33f8cd51db 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -531,7 +531,7 @@ async def async_turn_aux_heat_off(self) -> None: async def async_turn_on(self) -> None: """Turn the entity on.""" if hasattr(self, "turn_on"): - await self.hass.async_add_executor_job(self.turn_on) # type: ignore[attr-defined] + await self.hass.async_add_executor_job(self.turn_on) return # Fake turn on @@ -544,7 +544,7 @@ async def async_turn_on(self) -> None: async def async_turn_off(self) -> None: """Turn the entity off.""" if hasattr(self, "turn_off"): - await self.hass.async_add_executor_job(self.turn_off) # type: ignore[attr-defined] + await self.hass.async_add_executor_job(self.turn_off) return # Fake turn off diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index fa014335df97f5..2ca37cc39ebf81 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -500,7 +500,7 @@ def register_event_callback(self) -> TelegramQueue.Callback: transcoder := DPTBase.parse_transcoder(dpt) ): self._address_filter_transcoder.update( - {_filter: transcoder for _filter in _filters} # type: ignore[misc] + {_filter: transcoder for _filter in _filters} # type: ignore[type-abstract] ) return self.xknx.telegram_queue.register_telegram_received_cb( @@ -532,7 +532,7 @@ async def service_event_register_modify(self, call: ServiceCall) -> None: transcoder := DPTBase.parse_transcoder(dpt) ): self._group_address_transcoder.update( - {_address: transcoder for _address in group_addresses} # type: ignore[misc] + {_address: transcoder for _address in group_addresses} # type: ignore[type-abstract] ) for group_address in group_addresses: if group_address in self._knx_event_callback.group_addresses: diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index 13f6f153dafeb5..3850119f387678 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -79,8 +79,8 @@ def dpt_value_validator(value: Any) -> str | int: return dpt_value_validator -numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[misc] -sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[misc] +numeric_type_validator = dpt_subclass_validator(DPTNumeric) # type: ignore[type-abstract] +sensor_type_validator = dpt_subclass_validator(DPTBase) # type: ignore[type-abstract] string_type_validator = dpt_subclass_validator(DPTString) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 5771e1b6938de6..c43c9980c19e68 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -920,9 +920,7 @@ def support_grouping(self) -> bool: async def async_toggle(self) -> None: """Toggle the power on the media player.""" if hasattr(self, "toggle"): - await self.hass.async_add_executor_job( - self.toggle # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.toggle) return if self.state in { @@ -940,9 +938,7 @@ async def async_volume_up(self) -> None: This method is a coroutine. """ if hasattr(self, "volume_up"): - await self.hass.async_add_executor_job( - self.volume_up # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.volume_up) return if ( @@ -958,9 +954,7 @@ async def async_volume_down(self) -> None: This method is a coroutine. """ if hasattr(self, "volume_down"): - await self.hass.async_add_executor_job( - self.volume_down # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.volume_down) return if ( @@ -973,9 +967,7 @@ async def async_volume_down(self) -> None: async def async_media_play_pause(self) -> None: """Play or pause the media player.""" if hasattr(self, "media_play_pause"): - await self.hass.async_add_executor_job( - self.media_play_pause # type: ignore[attr-defined] - ) + await self.hass.async_add_executor_job(self.media_play_pause) return if self.state == MediaPlayerState.PLAYING: diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index c3bb02896e0eb5..0172fbc98d028f 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -282,7 +282,7 @@ async def async_register_services(self) -> None: if hasattr(self, "targets"): stale_targets = set(self.registered_targets) - for name, target in self.targets.items(): # type: ignore[attr-defined] + for name, target in self.targets.items(): target_name = slugify(f"{self._target_service_name_prefix}_{name}") if target_name in stale_targets: stale_targets.remove(target_name) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index cfc797cf7ea37d..3ab8b890838360 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -160,7 +160,7 @@ def attributes(self, value: dict[str, Any]) -> None: """Set attributes.""" self._attributes = value - @property # type: ignore[override] + @property def context(self) -> Context: """State context.""" if self._context is None: @@ -172,7 +172,7 @@ def context(self, value: Context) -> None: """Set context.""" self._context = value - @property # type: ignore[override] + @property def last_changed(self) -> datetime: """Last changed datetime.""" if self._last_changed is None: @@ -187,7 +187,7 @@ def last_changed(self, value: datetime) -> None: """Set last changed datetime.""" self._last_changed = value - @property # type: ignore[override] + @property def last_updated(self) -> datetime: """Last updated datetime.""" if self._last_updated is None: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 57cfe362231491..d4f8128f643fd0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -705,9 +705,9 @@ async def async_device_update(self, warning: bool = True) -> None: try: task: asyncio.Future[None] if hasattr(self, "async_update"): - task = self.hass.async_create_task(self.async_update()) # type: ignore[attr-defined] + task = self.hass.async_create_task(self.async_update()) elif hasattr(self, "update"): - task = self.hass.async_add_executor_job(self.update) # type: ignore[attr-defined] + task = self.hass.async_add_executor_job(self.update) else: return diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index 9f69c6c346e2d1..db8b496d90ecb9 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -12,7 +12,9 @@ try: from yaml import CSafeDumper as FastestAvailableSafeDumper except ImportError: - from yaml import SafeDumper as FastestAvailableSafeDumper # type: ignore[misc] + from yaml import ( # type: ignore[assignment] + SafeDumper as FastestAvailableSafeDumper, + ) def dump(_dict: dict) -> str: diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 09e19af684074b..62d754329c44b2 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -18,7 +18,7 @@ HAS_C_LOADER = True except ImportError: HAS_C_LOADER = False - from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[misc] + from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[assignment] from homeassistant.exceptions import HomeAssistantError diff --git a/mypy.ini b/mypy.ini index 3f0e917f167977..988701393d6429 100644 --- a/mypy.ini +++ b/mypy.ini @@ -14,6 +14,7 @@ warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true enable_error_code = ignore-without-code +disable_error_code = annotation-unchecked strict_concatenate = false check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_test.txt b/requirements_test.txt index 06bd7e878ce2c1..e0cbafbd6d4994 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ codecov==2.1.12 coverage==6.4.4 freezegun==1.2.2 mock-open==1.4.0 -mypy==0.982 +mypy==0.990 pre-commit==2.20.0 pylint==2.15.5 pipdeptree==2.3.1 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 0c598df9cd1ef6..99e07bcf1183c4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -46,7 +46,8 @@ "warn_redundant_casts": "true", "warn_unused_configs": "true", "warn_unused_ignores": "true", - "enable_error_code": "ignore-without-code", + "enable_error_code": ", ".join(["ignore-without-code"]), + "disable_error_code": ", ".join(["annotation-unchecked"]), # Strict_concatenate breaks passthrough ParamSpec typing "strict_concatenate": "false", } From 3edaef63b00a8c95abf4034f50f7f2be81f7c85e Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Nov 2022 00:47:00 +1100 Subject: [PATCH 316/394] Add integration_type to ign_sismologia (#81729) define integration type --- homeassistant/components/ign_sismologia/manifest.json | 3 ++- homeassistant/generated/integrations.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index 97836e7f1451e4..927e52f594dff6 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -5,5 +5,6 @@ "requirements": ["georss_ign_sismologia_client==0.3"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["georss_ign_sismologia_client"] + "loggers": ["georss_ign_sismologia_client"], + "integration_type": "service" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 21e19326dc691d..91f19977ccb98c 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2349,7 +2349,7 @@ }, "ign_sismologia": { "name": "IGN Sismolog\u00eda", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "cloud_polling" }, From 014c2d487d1075724fce3396a1c9eda36b823721 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Nov 2022 15:00:30 +0100 Subject: [PATCH 317/394] Update frontend to 20221108.0 (#81787) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f4f46a1f89b304..ec7001006b12c4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20221102.1"], + "requirements": ["home-assistant-frontend==20221108.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4a6b8b076f7abe..66ffb1eb85ba75 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ dbus-fast==1.72.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 512c366f5dc300..9946af9a968a24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -884,7 +884,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc44c0c0b6b402..9df97b9a7aea58 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -664,7 +664,7 @@ hole==0.7.0 holidays==0.16 # homeassistant.components.frontend -home-assistant-frontend==20221102.1 +home-assistant-frontend==20221108.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 45be2a260e6e9cc48e635918332c697d489dd52f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 8 Nov 2022 07:41:09 -0700 Subject: [PATCH 318/394] Add re-auth flow for OpenUV (#79691) --- homeassistant/components/openuv/__init__.py | 5 +- .../components/openuv/config_flow.py | 123 +++++++++++++++--- .../components/openuv/coordinator.py | 83 +++++++++++- homeassistant/components/openuv/strings.json | 10 +- .../components/openuv/translations/en.json | 10 +- tests/components/openuv/test_config_flow.py | 24 +++- 6 files changed, 228 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index c4a9d347a40165..3e65f33d8c5512 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -31,7 +31,7 @@ DOMAIN, LOGGER, ) -from .coordinator import OpenUvCoordinator +from .coordinator import InvalidApiKeyMonitor, OpenUvCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -53,6 +53,8 @@ async def async_update_protection_data() -> dict[str, Any]: high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW) return await client.uv_protection_window(low=low, high=high) + invalid_api_key_monitor = InvalidApiKeyMonitor(hass, entry) + coordinators: dict[str, OpenUvCoordinator] = { coordinator_name: OpenUvCoordinator( hass, @@ -60,6 +62,7 @@ async def async_update_protection_data() -> dict[str, Any]: latitude=client.latitude, longitude=client.longitude, update_method=update_method, + invalid_api_key_monitor=invalid_api_key_monitor, ) for coordinator_name, update_method in ( (DATA_UV, client.uv_index), diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index facbc37986eafd..2e96ce7c292689 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the OpenUV component.""" from __future__ import annotations +from collections.abc import Mapping +from dataclasses import dataclass from typing import Any from pyopenuv import Client @@ -27,14 +29,39 @@ DOMAIN, ) +STEP_REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } +) + + +@dataclass +class OpenUvData: + """Define structured OpenUV data needed to create/re-auth an entry.""" + + api_key: str + latitude: float + longitude: float + elevation: float + + @property + def unique_id(self) -> str: + """Return the unique for this data.""" + return f"{self.latitude}, {self.longitude}" + class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an OpenUV config flow.""" VERSION = 2 + def __init__(self) -> None: + """Initialize.""" + self._reauth_data: Mapping[str, Any] = {} + @property - def config_schema(self) -> vol.Schema: + def step_user_schema(self) -> vol.Schema: """Return the config schema.""" return vol.Schema( { @@ -51,13 +78,41 @@ def config_schema(self) -> vol.Schema: } ) - async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: - """Show the form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=self.config_schema, - errors=errors if errors else {}, - ) + async def _async_verify( + self, data: OpenUvData, error_step_id: str, error_schema: vol.Schema + ) -> FlowResult: + """Verify the credentials and create/re-auth the entry.""" + websession = aiohttp_client.async_get_clientsession(self.hass) + client = Client(data.api_key, 0, 0, session=websession) + client.disable_request_retries() + + try: + await client.uv_index() + except OpenUvError: + return self.async_show_form( + step_id=error_step_id, + data_schema=error_schema, + errors={CONF_API_KEY: "invalid_api_key"}, + description_placeholders={ + CONF_LATITUDE: str(data.latitude), + CONF_LONGITUDE: str(data.longitude), + }, + ) + + entry_data = { + CONF_API_KEY: data.api_key, + CONF_LATITUDE: data.latitude, + CONF_LONGITUDE: data.longitude, + CONF_ELEVATION: data.elevation, + } + + if existing_entry := await self.async_set_unique_id(data.unique_id): + self.hass.config_entries.async_update_entry(existing_entry, data=entry_data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(existing_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=data.unique_id, data=entry_data) @staticmethod @callback @@ -65,26 +120,54 @@ def async_get_options_flow(config_entry: ConfigEntry) -> OpenUvOptionsFlowHandle """Define the config flow to handle options.""" return OpenUvOptionsFlowHandler(config_entry) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauth_data = entry_data + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-auth completion.""" + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_REAUTH_SCHEMA, + description_placeholders={ + CONF_LATITUDE: self._reauth_data[CONF_LATITUDE], + CONF_LONGITUDE: self._reauth_data[CONF_LONGITUDE], + }, + ) + + data = OpenUvData( + user_input[CONF_API_KEY], + self._reauth_data[CONF_LATITUDE], + self._reauth_data[CONF_LONGITUDE], + self._reauth_data[CONF_ELEVATION], + ) + + return await self._async_verify(data, "reauth_confirm", STEP_REAUTH_SCHEMA) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the start of the config flow.""" if not user_input: - return await self._show_form() + return self.async_show_form( + step_id="user", data_schema=self.step_user_schema + ) + + data = OpenUvData( + user_input[CONF_API_KEY], + user_input[CONF_LATITUDE], + user_input[CONF_LONGITUDE], + user_input[CONF_ELEVATION], + ) - identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" - await self.async_set_unique_id(identifier) + await self.async_set_unique_id(data.unique_id) self._abort_if_unique_id_configured() - websession = aiohttp_client.async_get_clientsession(self.hass) - client = Client(user_input[CONF_API_KEY], 0, 0, session=websession) - - try: - await client.uv_index() - except OpenUvError: - return await self._show_form({CONF_API_KEY: "invalid_api_key"}) - - return self.async_create_entry(title=identifier, data=user_input) + return await self._async_verify(data, "user", self.step_user_schema) class OpenUvOptionsFlowHandler(config_entries.OptionsFlow): diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index 993970658efd76..36267972f80481 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -1,23 +1,94 @@ """Define an update coordinator for OpenUV.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from typing import Any, cast -from pyopenuv.errors import OpenUvError +from pyopenuv.errors import InvalidApiKeyError, OpenUvError -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import DOMAIN, LOGGER DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 +class InvalidApiKeyMonitor: + """Define a monitor for failed API calls (due to bad keys) across coordinators.""" + + DEFAULT_FAILED_API_CALL_THRESHOLD = 5 + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self._count = 1 + self._lock = asyncio.Lock() + self._reauth_flow_manager = ReauthFlowManager(hass, entry) + self.entry = entry + + async def async_increment(self) -> None: + """Increment the counter.""" + LOGGER.debug("Invalid API key response detected (number %s)", self._count) + async with self._lock: + self._count += 1 + if self._count > self.DEFAULT_FAILED_API_CALL_THRESHOLD: + self._reauth_flow_manager.start_reauth() + + async def async_reset(self) -> None: + """Reset the counter.""" + async with self._lock: + self._count = 0 + self._reauth_flow_manager.cancel_reauth() + + +class ReauthFlowManager: + """Define an OpenUV reauth flow manager.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self.entry = entry + self.hass = hass + + @callback + def _get_active_reauth_flow(self) -> FlowResult | None: + """Get an active reauth flow (if it exists).""" + try: + [reauth_flow] = [ + flow + for flow in self.hass.config_entries.flow.async_progress_by_handler( + DOMAIN + ) + if flow["context"]["source"] == "reauth" + and flow["context"]["entry_id"] == self.entry.entry_id + ] + except ValueError: + return None + + return reauth_flow + + @callback + def cancel_reauth(self) -> None: + """Cancel a reauth flow (if appropriate).""" + if reauth_flow := self._get_active_reauth_flow(): + LOGGER.debug("API seems to have recovered; canceling reauth flow") + self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"]) + + @callback + def start_reauth(self) -> None: + """Start a reauth flow (if appropriate).""" + if not self._get_active_reauth_flow(): + LOGGER.debug("Multiple API failures in a row; starting reauth flow") + self.entry.async_start_reauth(self.hass) + + class OpenUvCoordinator(DataUpdateCoordinator): """Define an OpenUV data coordinator.""" + config_entry: ConfigEntry update_method: Callable[[], Awaitable[dict[str, Any]]] def __init__( @@ -28,6 +99,7 @@ def __init__( latitude: str, longitude: str, update_method: Callable[[], Awaitable[dict[str, Any]]], + invalid_api_key_monitor: InvalidApiKeyMonitor, ) -> None: """Initialize.""" super().__init__( @@ -43,6 +115,7 @@ def __init__( ), ) + self._invalid_api_key_monitor = invalid_api_key_monitor self.latitude = latitude self.longitude = longitude @@ -50,6 +123,10 @@ async def _async_update_data(self) -> dict[str, Any]: """Fetch data from OpenUV.""" try: data = await self.update_method() + except InvalidApiKeyError: + await self._invalid_api_key_monitor.async_increment() except OpenUvError as err: raise UpdateFailed(f"Error during protection data update: {err}") from err + + await self._invalid_api_key_monitor.async_reset() return cast(dict[str, Any], data["result"]) diff --git a/homeassistant/components/openuv/strings.json b/homeassistant/components/openuv/strings.json index 84a093280f3d4f..9542cb8b1a7142 100644 --- a/homeassistant/components/openuv/strings.json +++ b/homeassistant/components/openuv/strings.json @@ -1,6 +1,13 @@ { "config": { "step": { + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please re-enter the API key for {latitude}, {longitude}.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, "user": { "title": "Fill in your information", "data": { @@ -15,7 +22,8 @@ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/openuv/translations/en.json b/homeassistant/components/openuv/translations/en.json index 3879a4d7d44692..9db83868543fc2 100644 --- a/homeassistant/components/openuv/translations/en.json +++ b/homeassistant/components/openuv/translations/en.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Location is already configured" + "already_configured": "Location is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_api_key": "Invalid API key" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "Please re-enter the API key for {latitude}, {longitude}.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "api_key": "API Key", diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 9f51728365b388..eeafd82a20f094 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import data_entry_flow from homeassistant.components.openuv import CONF_FROM_WINDOW, CONF_TO_WINDOW, DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_ELEVATION, @@ -51,6 +51,28 @@ async def test_options_flow(hass, config_entry): assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} +async def test_step_reauth(hass, config, config_entry, setup_openuv): + """Test that the reauth step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=config + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_API_KEY: "new_api_key"} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert len(hass.config_entries.async_entries()) == 1 + + async def test_step_user(hass, config, setup_openuv): """Test that the user step works.""" result = await hass.config_entries.flow.async_init( From 5040b943305e4cdb2fb0afec7df7bf607ce44aa2 Mon Sep 17 00:00:00 2001 From: ztamas83 <71548739+ztamas83@users.noreply.github.com> Date: Tue, 8 Nov 2022 16:09:58 +0100 Subject: [PATCH 319/394] Retry tibber setup (#81785) * Handle integration setup retries * Fix black error * Update to falsy check Co-authored-by: Martin Hjelmare * Remove duplicated log * Update exception message Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/tibber/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 35507986f90d2e..4d9c05606828e2 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -53,6 +53,9 @@ async def _close(event: Event) -> None: try: await tibber_connection.update_info() + if not tibber_connection.name: + raise ConfigEntryNotReady("Could not fetch Tibber data.") + except asyncio.TimeoutError as err: raise ConfigEntryNotReady from err except aiohttp.ClientError as err: From 88a7c76739d076ada1aec8886e187364841fcb8d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 8 Nov 2022 17:16:31 +0200 Subject: [PATCH 320/394] Fix late-import paho (#81791) fix: late-import MQTTMessage `paho-mqtt` is not listed in main requirements and is imported early by `conftest`. Import it late to avoid an ImportError. Split out from #81678. --- homeassistant/components/mqtt/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index e909a378581465..34384614c20255 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -15,7 +15,6 @@ import attr import certifi -from paho.mqtt.client import MQTTMessage from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -620,7 +619,7 @@ async def publish_birth_message(birth_message: PublishMessage) -> None: ) def _mqtt_on_message( - self, _mqttc: mqtt.Client, _userdata: None, msg: MQTTMessage + self, _mqttc: mqtt.Client, _userdata: None, msg: mqtt.MQTTMessage ) -> None: """Message received callback.""" self.hass.add_job(self._mqtt_handle_message, msg) @@ -634,7 +633,7 @@ def _matching_subscriptions(self, topic: str) -> list[Subscription]: return subscriptions @callback - def _mqtt_handle_message(self, msg: MQTTMessage) -> None: + def _mqtt_handle_message(self, msg: mqtt.MQTTMessage) -> None: _LOGGER.debug( "Received%s message on %s: %s", " retained" if msg.retain else "", From 2cb58b818955a3291e25da62f1b06c2b8f2ce248 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Nov 2022 09:20:03 -0600 Subject: [PATCH 321/394] Fix off by one in HomeKit iid allocator (#81793) --- homeassistant/components/homekit/iidmanager.py | 8 ++++---- tests/components/homekit/test_diagnostics.py | 2 -- tests/components/homekit/test_iidmanager.py | 6 ++++++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/iidmanager.py b/homeassistant/components/homekit/iidmanager.py index 3805748225a0cb..8bac268800b802 100644 --- a/homeassistant/components/homekit/iidmanager.py +++ b/homeassistant/components/homekit/iidmanager.py @@ -109,12 +109,12 @@ def get_or_allocate_iid( # AID must be a string since JSON keys cannot be int aid_str = str(aid) accessory_allocation = self.allocations.setdefault(aid_str, {}) - accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, []) + accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1]) if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None: - allocated_iid = 1 - elif allocation_key in accessory_allocation: + return 1 + if allocation_key in accessory_allocation: return accessory_allocation[allocation_key] - elif accessory_allocated_iids: + if accessory_allocated_iids: allocated_iid = accessory_allocated_iids[-1] + 1 else: allocated_iid = 2 diff --git a/tests/components/homekit/test_diagnostics.py b/tests/components/homekit/test_diagnostics.py index be98c3bacdd77f..d114c462e2f2cf 100644 --- a/tests/components/homekit/test_diagnostics.py +++ b/tests/components/homekit/test_diagnostics.py @@ -51,7 +51,6 @@ async def test_config_entry_running(hass, hass_client, hk_driver, mock_async_zer "3E__23_": 5, "3E__30_": 6, "3E__52_": 7, - "3E___": 1, "A2__37_": 9, "A2___": 8, } @@ -278,7 +277,6 @@ async def test_config_entry_accessory( "3E__23_": 5, "3E__30_": 6, "3E__52_": 7, - "3E___": 1, "43__25_": 11, "43___": 10, "A2__37_": 9, diff --git a/tests/components/homekit/test_iidmanager.py b/tests/components/homekit/test_iidmanager.py index 3e4a19c9045ad8..c16cbf01d7da3d 100644 --- a/tests/components/homekit/test_iidmanager.py +++ b/tests/components/homekit/test_iidmanager.py @@ -152,23 +152,29 @@ async def test_iid_generation_and_restore_v2(hass, iid_storage, hass_storage): 1, "000000AA-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid == 2 + assert iid_storage.allocated_iids == {"1": [1, 2]} not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid_2 == 3 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} not_accessory_info_service_iid_2 = iid_storage.get_or_allocate_iid( 1, "000000BB-0000-1000-8000-0026BB765291", None, None, None ) assert not_accessory_info_service_iid_2 == 3 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 1, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3]} accessory_info_service_iid = iid_storage.get_or_allocate_iid( 2, "0000003E-0000-1000-8000-0026BB765291", None, None, None ) assert accessory_info_service_iid == 1 + assert iid_storage.allocated_iids == {"1": [1, 2, 3], "2": [1]} From 7d768dc3a6020e645ca29e8e8846f7869eb0a48a Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 17:02:06 +0100 Subject: [PATCH 322/394] Improve MQTT type hints / refactor part 7 - trigger (#81019) * Improve typing trigger * Improve hints on templates * Use new sentinel for template * Follow-up comment --- homeassistant/components/mqtt/trigger.py | 74 +++++++++++++----------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 3530538122d863..c10e539f8ec665 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -1,21 +1,31 @@ """Offer MQTT listening automation rules.""" +from __future__ import annotations + +from collections.abc import Callable from contextlib import suppress import logging +from typing import Any import voluptuous as vol from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import json_loads -from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.template import Template +from homeassistant.helpers.trigger import TriggerActionType, TriggerData, TriggerInfo +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .. import mqtt from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING, DEFAULT_QOS - -# mypy: allow-untyped-defs - +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PayloadSentinel, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( { @@ -40,43 +50,37 @@ async def async_attach_trigger( trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = trigger_info["trigger_data"] - topic = config[CONF_TOPIC] - wanted_payload = config.get(CONF_PAYLOAD) - value_template = config.get(CONF_VALUE_TEMPLATE) - encoding = config[CONF_ENCODING] or None - qos = config[CONF_QOS] + trigger_data: TriggerData = trigger_info["trigger_data"] + command_template: Callable[ + [PublishPayloadType, TemplateVarsType], PublishPayloadType + ] = MqttCommandTemplate(config.get(CONF_PAYLOAD), hass=hass).async_render + value_template: Callable[[ReceivePayloadType, str], ReceivePayloadType] + value_template = MqttValueTemplate( + config.get(CONF_VALUE_TEMPLATE), hass=hass + ).async_render_with_possible_json_value + encoding: str | None = config[CONF_ENCODING] or None + qos: int = config[CONF_QOS] job = HassJob(action) - variables = None + variables: TemplateVarsType | None = None if trigger_info: variables = trigger_info.get("variables") - template.attach(hass, wanted_payload) - if wanted_payload: - wanted_payload = wanted_payload.async_render( - variables, limited=True, parse_result=False - ) + wanted_payload = command_template(None, variables) - template.attach(hass, topic) - if isinstance(topic, template.Template): - topic = topic.async_render(variables, limited=True, parse_result=False) - topic = mqtt.util.valid_subscribe_topic(topic) - - template.attach(hass, value_template) + topic_template: Template = config[CONF_TOPIC] + topic_template.hass = hass + topic = topic_template.async_render(variables, limited=True, parse_result=False) + mqtt.util.valid_subscribe_topic(topic) @callback - def mqtt_automation_listener(mqttmsg): + def mqtt_automation_listener(mqttmsg: ReceiveMessage) -> None: """Listen for MQTT messages.""" - payload = mqttmsg.payload - - if value_template is not None: - payload = value_template.async_render_with_possible_json_value( - payload, - error_value=None, - ) - - if wanted_payload is None or wanted_payload == payload: - data = { + if wanted_payload is None or ( + (payload := value_template(mqttmsg.payload, PayloadSentinel.DEFAULT)) + and payload is not PayloadSentinel.DEFAULT + and wanted_payload == payload + ): + data: dict[str, Any] = { **trigger_data, "platform": "mqtt", "topic": mqttmsg.topic, From 3cc9ecf1dc0dc5fdace19770c076eb47d2f1c746 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 17:18:40 +0100 Subject: [PATCH 323/394] Implement ConfigEntry async_wait_for_states (#81771) * Implement async_wait_for_states * stale docstr, remove hints * Assert return states for tests --- homeassistant/config_entries.py | 34 +++++++++- tests/test_config_entries.py | 107 ++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bd985517ca74d6..902fa0d03f2c1c 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -12,6 +12,8 @@ from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast import weakref +import async_timeout + from . import data_entry_flow, loader from .backports.enum import StrEnum from .components import persistent_notification @@ -19,7 +21,7 @@ from .core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError from .helpers import device_registry, entity_registry, storage -from .helpers.dispatcher import async_dispatcher_send +from .helpers.dispatcher import async_dispatcher_connect, async_dispatcher_send from .helpers.event import async_call_later from .helpers.frame import report from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType @@ -1239,6 +1241,36 @@ async def async_forward_entry_setup( await entry.async_setup(self.hass, integration=integration) return True + async def async_wait_for_states( + self, entry: ConfigEntry, states: set[ConfigEntryState], timeout: float = 60.0 + ) -> ConfigEntryState: + """Wait for the setup of an entry to reach one of the supplied states state. + + Returns the state the entry reached or raises asyncio.TimeoutError if the + entry did not reach one of the supplied states within the timeout. + """ + state_reached_future: asyncio.Future[ConfigEntryState] = asyncio.Future() + + @callback + def _async_entry_changed( + change: ConfigEntryChange, event_entry: ConfigEntry + ) -> None: + if ( + event_entry is entry + and change is ConfigEntryChange.UPDATED + and entry.state in states + ): + state_reached_future.set_result(entry.state) + + unsub = async_dispatcher_connect( + self.hass, SIGNAL_CONFIG_ENTRY_CHANGED, _async_entry_changed + ) + try: + async with async_timeout.timeout(timeout): + return await state_reached_future + finally: + unsub() + async def async_unload_platforms( self, entry: ConfigEntry, platforms: Iterable[Platform | str] ) -> bool: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 99e26be6d750a6..7684e9ff260d2b 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3330,3 +3330,110 @@ async def test_reauth(hass): entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 2 + + +async def test_wait_for_loading_entry(hass): + """Test waiting for entry to be set up.""" + + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + flow = hass.config_entries.flow + + async def _load_entry(): + # Mock config entry + assert await async_setup_component(hass, "test", {}) + + entry = MockConfigEntry(title="test_title", domain="test") + entry.add_to_hass(hass) + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init): + hass.async_add_job(_load_entry) + new_state = await hass.config_entries.async_wait_for_states( + entry, + { + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + }, + timeout=1.0, + ) + assert new_state is config_entries.ConfigEntryState.LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED + + +async def test_wait_for_loading_failed_entry(hass): + """Test waiting for entry to be set up that fails loading.""" + + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(side_effect=HomeAssistantError) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + flow = hass.config_entries.flow + + async def _load_entry(): + # Mock config entry + assert await async_setup_component(hass, "test", {}) + + entry = MockConfigEntry(title="test_title", domain="test") + entry.add_to_hass(hass) + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init): + hass.async_add_job(_load_entry) + new_state = await hass.config_entries.async_wait_for_states( + entry, + { + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + }, + timeout=1.0, + ) + assert new_state is config_entries.ConfigEntryState.SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR + + +async def test_wait_for_loading_timeout(hass): + """Test waiting for entry to be set up that fails with a timeout.""" + + async def _async_setup_entry(hass, entry): + await asyncio.sleep(1) + return True + + entry = MockConfigEntry(title="test_title", domain="test") + + mock_integration(hass, MockModule("test", async_setup_entry=_async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + flow = hass.config_entries.flow + + async def _load_entry(): + # Mock config entry + assert await async_setup_component(hass, "test", {}) + + entry = MockConfigEntry(title="test_title", domain="test") + entry.add_to_hass(hass) + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init): + hass.async_add_job(_load_entry) + with pytest.raises(asyncio.exceptions.TimeoutError): + await hass.config_entries.async_wait_for_states( + entry, + { + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + }, + timeout=0.1, + ) From b364ef98a073214aad8deff4ff9b91e9ff041557 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 18:48:08 +0100 Subject: [PATCH 324/394] Use `_attr_` for MQTT vacuum (#81534) * Use `_attr_` for MQTT vacuum * Remove unneeded properties * Follow-up comment * Remove default value --- .../components/mqtt/vacuum/schema_legacy.py | 82 ++++++------------- .../components/mqtt/vacuum/schema_state.py | 58 +++++-------- 2 files changed, 45 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index a15367c3cadc19..4425a3775d91a5 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -173,14 +173,13 @@ class MqttVacuum(MqttEntity, VacuumEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the vacuum.""" - self._cleaning = False + self._attr_battery_level = 0 + self._attr_is_on = False + self._attr_fan_speed = "unknown" + self._charging = False self._docked = False self._error = None - self._status = "Unknown" - self._battery_level = 0 - self._fan_speed = "unknown" - self._fan_speed_list = [] MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -190,11 +189,12 @@ def config_schema(): return DISCOVERY_SCHEMA_LEGACY def _setup_from_config(self, config): + """(Re)Setup the entity.""" supported_feature_strings = config[CONF_SUPPORTED_FEATURES] - self._supported_features = strings_to_services( + self._attr_supported_features = strings_to_services( supported_feature_strings, STRING_TO_SERVICE ) - self._fan_speed_list = config[CONF_FAN_SPEED_LIST] + self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] self._qos = config[CONF_QOS] self._retain = config[CONF_RETAIN] self._encoding = config[CONF_ENCODING] or None @@ -258,7 +258,7 @@ def message_received(msg: ReceiveMessage) -> None: msg.payload, PayloadSentinel.DEFAULT ) if battery_level and battery_level is not PayloadSentinel.DEFAULT: - self._battery_level = int(battery_level) + self._attr_battery_level = max(0, min(100, int(battery_level))) if ( msg.topic == self._state_topics[CONF_CHARGING_TOPIC] @@ -282,7 +282,7 @@ def message_received(msg: ReceiveMessage) -> None: msg.payload, PayloadSentinel.DEFAULT ) if cleaning and cleaning is not PayloadSentinel.DEFAULT: - self._cleaning = cv.boolean(cleaning) + self._attr_is_on = cv.boolean(cleaning) if ( msg.topic == self._state_topics[CONF_DOCKED_TOPIC] @@ -310,15 +310,15 @@ def message_received(msg: ReceiveMessage) -> None: if self._docked: if self._charging: - self._status = "Docked & Charging" + self._attr_status = "Docked & Charging" else: - self._status = "Docked" - elif self._cleaning: - self._status = "Cleaning" + self._attr_status = "Docked" + elif self.is_on: + self._attr_status = "Cleaning" elif self._error: - self._status = f"Error: {self._error}" + self._attr_status = f"Error: {self._error}" else: - self._status = "Stopped" + self._attr_status = "Stopped" if ( msg.topic == self._state_topics[CONF_FAN_SPEED_TOPIC] @@ -330,7 +330,7 @@ def message_received(msg: ReceiveMessage) -> None: msg.payload, PayloadSentinel.DEFAULT ) if fan_speed and fan_speed is not PayloadSentinel.DEFAULT: - self._fan_speed = fan_speed + self._attr_fan_speed = fan_speed get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -353,31 +353,6 @@ async def _subscribe_topics(self): """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - @property - def is_on(self): - """Return true if vacuum is on.""" - return self._cleaning - - @property - def status(self): - """Return a status string for the vacuum.""" - return self._status - - @property - def fan_speed(self): - """Return the status of the vacuum.""" - return self._fan_speed - - @property - def fan_speed_list(self): - """Return the status of the vacuum.""" - return self._fan_speed_list - - @property - def battery_level(self): - """Return the status of the vacuum.""" - return max(0, min(100, self._battery_level)) - @property def battery_icon(self): """Return the battery icon for the vacuum cleaner. @@ -388,11 +363,6 @@ def battery_icon(self): battery_level=self.battery_level, charging=self._charging ) - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - async def async_turn_on(self, **kwargs): """Turn the vacuum on.""" if self.supported_features & VacuumEntityFeature.TURN_ON == 0: @@ -405,7 +375,7 @@ async def async_turn_on(self, **kwargs): self._retain, self._encoding, ) - self._status = "Cleaning" + self._attr_status = "Cleaning" self.async_write_ha_state() async def async_turn_off(self, **kwargs): @@ -420,7 +390,7 @@ async def async_turn_off(self, **kwargs): self._retain, self._encoding, ) - self._status = "Turning Off" + self._attr_status = "Turning Off" self.async_write_ha_state() async def async_stop(self, **kwargs): @@ -435,7 +405,7 @@ async def async_stop(self, **kwargs): self._retain, self._encoding, ) - self._status = "Stopping the current task" + self._attr_status = "Stopping the current task" self.async_write_ha_state() async def async_clean_spot(self, **kwargs): @@ -450,7 +420,7 @@ async def async_clean_spot(self, **kwargs): self._retain, self._encoding, ) - self._status = "Cleaning spot" + self._attr_status = "Cleaning spot" self.async_write_ha_state() async def async_locate(self, **kwargs): @@ -465,7 +435,7 @@ async def async_locate(self, **kwargs): self._retain, self._encoding, ) - self._status = "Hi, I'm over here!" + self._attr_status = "Hi, I'm over here!" self.async_write_ha_state() async def async_start_pause(self, **kwargs): @@ -480,7 +450,7 @@ async def async_start_pause(self, **kwargs): self._retain, self._encoding, ) - self._status = "Pausing/Resuming cleaning..." + self._attr_status = "Pausing/Resuming cleaning..." self.async_write_ha_state() async def async_return_to_base(self, **kwargs): @@ -495,14 +465,14 @@ async def async_return_to_base(self, **kwargs): self._retain, self._encoding, ) - self._status = "Returning home..." + self._attr_status = "Returning home..." self.async_write_ha_state() async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if ( self.supported_features & VacuumEntityFeature.FAN_SPEED == 0 - ) or fan_speed not in self._fan_speed_list: + ) or fan_speed not in self.fan_speed_list: return None await self.async_publish( @@ -512,7 +482,7 @@ async def async_set_fan_speed(self, fan_speed, **kwargs): self._retain, self._encoding, ) - self._status = f"Setting fan to {fan_speed}..." + self._attr_status = f"Setting fan to {fan_speed}..." self.async_write_ha_state() async def async_send_command(self, command, params=None, **kwargs): @@ -532,5 +502,5 @@ async def async_send_command(self, command, params=None, **kwargs): self._retain, self._encoding, ) - self._status = f"Sending command {message}..." + self._attr_status = f"Sending command {message}..." self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 8dfaba801096a2..ede258102d7fb2 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -161,9 +161,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): def __init__(self, hass, config, config_entry, discovery_data): """Initialize the vacuum.""" - self._state = None self._state_attrs = {} - self._fan_speed_list = [] MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -173,11 +171,12 @@ def config_schema(): return DISCOVERY_SCHEMA_STATE def _setup_from_config(self, config): + """(Re)Setup the entity.""" supported_feature_strings = config[CONF_SUPPORTED_FEATURES] - self._supported_features = strings_to_services( + self._attr_supported_features = strings_to_services( supported_feature_strings, STRING_TO_SERVICE ) - self._fan_speed_list = config[CONF_FAN_SPEED_LIST] + self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] self._command_topic = config.get(CONF_COMMAND_TOPIC) self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) @@ -194,6 +193,12 @@ def _setup_from_config(self, config): ) } + def _update_state_attributes(self, payload): + """Update the entity state attributes.""" + self._state_attrs.update(payload) + self._attr_fan_speed = self._state_attrs.get(FAN_SPEED, 0) + self._attr_battery_level = max(0, min(100, self._state_attrs.get(BATTERY, 0))) + def _prepare_subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -206,11 +211,11 @@ def state_message_received(msg): if STATE in payload and ( payload[STATE] in POSSIBLE_STATES or payload[STATE] is None ): - self._state = ( + self._attr_state = ( POSSIBLE_STATES[payload[STATE]] if payload[STATE] else None ) del payload[STATE] - self._state_attrs.update(payload) + self._update_state_attributes(payload) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) if self._config.get(CONF_STATE_TOPIC): @@ -228,31 +233,6 @@ async def _subscribe_topics(self): """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - @property - def state(self): - """Return state of vacuum.""" - return self._state - - @property - def fan_speed(self): - """Return fan speed of the vacuum.""" - return self._state_attrs.get(FAN_SPEED, 0) - - @property - def fan_speed_list(self): - """Return fan speed list of the vacuum.""" - return self._fan_speed_list - - @property - def battery_level(self): - """Return battery level of the vacuum.""" - return max(0, min(100, self._state_attrs.get(BATTERY, 0))) - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - async def async_start(self): """Start the vacuum.""" if self.supported_features & VacuumEntityFeature.START == 0: @@ -268,7 +248,7 @@ async def async_start(self): async def async_pause(self): """Pause the vacuum.""" if self.supported_features & VacuumEntityFeature.PAUSE == 0: - return None + return await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_PAUSE], @@ -280,7 +260,7 @@ async def async_pause(self): async def async_stop(self, **kwargs): """Stop the vacuum.""" if self.supported_features & VacuumEntityFeature.STOP == 0: - return None + return await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_STOP], @@ -292,9 +272,9 @@ async def async_stop(self, **kwargs): async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if (self.supported_features & VacuumEntityFeature.FAN_SPEED == 0) or ( - fan_speed not in self._fan_speed_list + fan_speed not in self.fan_speed_list ): - return None + return await self.async_publish( self._set_fan_speed_topic, fan_speed, @@ -306,7 +286,7 @@ async def async_set_fan_speed(self, fan_speed, **kwargs): async def async_return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: - return None + return await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_RETURN_TO_BASE], @@ -318,7 +298,7 @@ async def async_return_to_base(self, **kwargs): async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: - return None + return await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_CLEAN_SPOT], @@ -330,7 +310,7 @@ async def async_clean_spot(self, **kwargs): async def async_locate(self, **kwargs): """Locate the vacuum (usually by playing a song).""" if self.supported_features & VacuumEntityFeature.LOCATE == 0: - return None + return await self.async_publish( self._command_topic, self._config[CONF_PAYLOAD_LOCATE], @@ -342,7 +322,7 @@ async def async_locate(self, **kwargs): async def async_send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: - return None + return if params: message = {"command": command} message.update(params) From 6021cedb09a2b7d3434d307bc2c124c51c8f731a Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 8 Nov 2022 21:54:01 +0200 Subject: [PATCH 325/394] deconz: Use partition instead of split where possible (#81804) * deconz: Use partition instead of split where possible With a smattering of code deduplication Split out of #81493 * Update homeassistant/components/deconz/util.py Co-authored-by: Robert Svensson Co-authored-by: Robert Svensson --- homeassistant/components/deconz/binary_sensor.py | 5 ++++- homeassistant/components/deconz/deconz_device.py | 5 ++--- homeassistant/components/deconz/number.py | 3 ++- homeassistant/components/deconz/sensor.py | 7 +++++-- homeassistant/components/deconz/util.py | 9 +++++++++ tests/components/deconz/test_logbook.py | 11 ++++++----- 6 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/deconz/util.py diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 6e0c4c86d21b81..ef78e8f141936b 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -33,6 +33,7 @@ from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry +from .util import serial_from_unique_id _SensorDeviceT = TypeVar("_SensorDeviceT", bound=PydeconzSensorBase) @@ -187,7 +188,9 @@ def async_update_unique_id( return if description.old_unique_id_suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' + unique_id = ( + f"{serial_from_unique_id(unique_id)}-{description.old_unique_id_suffix}" + ) if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index c2161baf100c48..6163db0dc65b45 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -17,6 +17,7 @@ from .const import DOMAIN as DECONZ_DOMAIN from .gateway import DeconzGateway +from .util import serial_from_unique_id _DeviceT = TypeVar( "_DeviceT", @@ -55,9 +56,7 @@ def unique_id(self) -> str: def serial(self) -> str | None: """Return a serial number for this device.""" assert isinstance(self._device, PydeconzDevice) - if not self._device.unique_id or self._device.unique_id.count(":") != 7: - return None - return self._device.unique_id.split("-", 1)[0] + return serial_from_unique_id(self._device.unique_id) @property def device_info(self) -> DeviceInfo | None: diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 789a155477a6e3..154c988e07cdfe 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -26,6 +26,7 @@ from .const import DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry +from .util import serial_from_unique_id T = TypeVar("T", Presence, PydeconzSensorBase) @@ -88,7 +89,7 @@ def async_update_unique_id( if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): return - unique_id = f'{unique_id.split("-", 1)[0]}-{description.key}' + unique_id = f"{serial_from_unique_id(unique_id)}-{description.key}" if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index f1bd011803099e..5305ea625b8d12 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -50,6 +50,7 @@ from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry +from .util import serial_from_unique_id PROVIDES_EXTRA_ATTRIBUTES = ( "battery", @@ -248,7 +249,9 @@ def async_update_unique_id( return if description.old_unique_id_suffix: - unique_id = f'{unique_id.split("-", 1)[0]}-{description.old_unique_id_suffix}' + unique_id = ( + f"{serial_from_unique_id(unique_id)}-{description.old_unique_id_suffix}" + ) if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) @@ -290,7 +293,7 @@ def async_add_sensor(_: EventType, sensor_id: str) -> None: sensor.type.startswith("CLIP") or (no_sensor_data and description.key != "battery") or ( - (unique_id := sensor.unique_id.rsplit("-", 1)[0]) + (unique_id := sensor.unique_id.rpartition("-")[0]) in known_device_entities[description.key] ) ): diff --git a/homeassistant/components/deconz/util.py b/homeassistant/components/deconz/util.py new file mode 100644 index 00000000000000..4e7b1e7739f1c7 --- /dev/null +++ b/homeassistant/components/deconz/util.py @@ -0,0 +1,9 @@ +"""Utilities for deCONZ integration.""" +from __future__ import annotations + + +def serial_from_unique_id(unique_id: str | None) -> str | None: + """Get a device serial number from a unique ID, if possible.""" + if not unique_id or unique_id.count(":") != 7: + return None + return unique_id.partition("-")[0] diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 9ba0799d04eefc..1680854302ba45 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -7,6 +7,7 @@ CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT, ) +from homeassistant.components.deconz.util import serial_from_unique_id from homeassistant.const import ( CONF_CODE, CONF_DEVICE_ID, @@ -60,7 +61,7 @@ async def test_humanifying_deconz_alarm_event(hass, aioclient_mock): device_registry = dr.async_get(hass) keypad_event_id = slugify(data["sensors"]["1"]["name"]) - keypad_serial = data["sensors"]["1"]["uniqueid"].split("-", 1)[0] + keypad_serial = serial_from_unique_id(data["sensors"]["1"]["uniqueid"]) keypad_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, keypad_serial)} ) @@ -131,25 +132,25 @@ async def test_humanifying_deconz_event(hass, aioclient_mock): device_registry = dr.async_get(hass) switch_event_id = slugify(data["sensors"]["1"]["name"]) - switch_serial = data["sensors"]["1"]["uniqueid"].split("-", 1)[0] + switch_serial = serial_from_unique_id(data["sensors"]["1"]["uniqueid"]) switch_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, switch_serial)} ) hue_remote_event_id = slugify(data["sensors"]["2"]["name"]) - hue_remote_serial = data["sensors"]["2"]["uniqueid"].split("-", 1)[0] + hue_remote_serial = serial_from_unique_id(data["sensors"]["2"]["uniqueid"]) hue_remote_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, hue_remote_serial)} ) xiaomi_cube_event_id = slugify(data["sensors"]["3"]["name"]) - xiaomi_cube_serial = data["sensors"]["3"]["uniqueid"].split("-", 1)[0] + xiaomi_cube_serial = serial_from_unique_id(data["sensors"]["3"]["uniqueid"]) xiaomi_cube_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, xiaomi_cube_serial)} ) faulty_event_id = slugify(data["sensors"]["4"]["name"]) - faulty_serial = data["sensors"]["4"]["uniqueid"].split("-", 1)[0] + faulty_serial = serial_from_unique_id(data["sensors"]["4"]["uniqueid"]) faulty_entry = device_registry.async_get_device( identifiers={(DECONZ_DOMAIN, faulty_serial)} ) From 12d76a8a4f1cbc9403798a32dd335d928dd61373 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 8 Nov 2022 22:14:17 +0100 Subject: [PATCH 326/394] Address late review of config entry wait for states tests (#81801) * Late review on tests #81771 * Clean-up duplicate assignment --- tests/test_config_entries.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 7684e9ff260d2b..080b3cdf5f1a04 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3332,7 +3332,7 @@ async def test_reauth(hass): assert len(hass.config_entries.flow.async_progress()) == 2 -async def test_wait_for_loading_entry(hass): +async def test_wait_for_loading_entry(hass: HomeAssistant) -> None: """Test waiting for entry to be set up.""" entry = MockConfigEntry(title="test_title", domain="test") @@ -3346,15 +3346,13 @@ async def test_wait_for_loading_entry(hass): flow = hass.config_entries.flow - async def _load_entry(): - # Mock config entry + async def _load_entry() -> None: assert await async_setup_component(hass, "test", {}) - entry = MockConfigEntry(title="test_title", domain="test") entry.add_to_hass(hass) flow = hass.config_entries.flow with patch.object(flow, "async_init", wraps=flow.async_init): - hass.async_add_job(_load_entry) + hass.async_create_task(_load_entry()) new_state = await hass.config_entries.async_wait_for_states( entry, { @@ -3367,7 +3365,7 @@ async def _load_entry(): assert entry.state is config_entries.ConfigEntryState.LOADED -async def test_wait_for_loading_failed_entry(hass): +async def test_wait_for_loading_failed_entry(hass: HomeAssistant) -> None: """Test waiting for entry to be set up that fails loading.""" entry = MockConfigEntry(title="test_title", domain="test") @@ -3376,20 +3374,17 @@ async def test_wait_for_loading_failed_entry(hass): mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) mock_entity_platform(hass, "config_flow.test", None) - await entry.async_setup(hass) await hass.async_block_till_done() flow = hass.config_entries.flow - async def _load_entry(): - # Mock config entry + async def _load_entry() -> None: assert await async_setup_component(hass, "test", {}) - entry = MockConfigEntry(title="test_title", domain="test") entry.add_to_hass(hass) flow = hass.config_entries.flow with patch.object(flow, "async_init", wraps=flow.async_init): - hass.async_add_job(_load_entry) + hass.async_create_task(_load_entry()) new_state = await hass.config_entries.async_wait_for_states( entry, { @@ -3402,7 +3397,7 @@ async def _load_entry(): assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR -async def test_wait_for_loading_timeout(hass): +async def test_wait_for_loading_timeout(hass: HomeAssistant) -> None: """Test waiting for entry to be set up that fails with a timeout.""" async def _async_setup_entry(hass, entry): @@ -3414,20 +3409,17 @@ async def _async_setup_entry(hass, entry): mock_integration(hass, MockModule("test", async_setup_entry=_async_setup_entry)) mock_entity_platform(hass, "config_flow.test", None) - await entry.async_setup(hass) await hass.async_block_till_done() flow = hass.config_entries.flow - async def _load_entry(): - # Mock config entry + async def _load_entry() -> None: assert await async_setup_component(hass, "test", {}) - entry = MockConfigEntry(title="test_title", domain="test") entry.add_to_hass(hass) flow = hass.config_entries.flow with patch.object(flow, "async_init", wraps=flow.async_init): - hass.async_add_job(_load_entry) + hass.async_create_task(_load_entry()) with pytest.raises(asyncio.exceptions.TimeoutError): await hass.config_entries.async_wait_for_states( entry, From 58c691be1e6f7966798a572c60579137b3c4cde6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 9 Nov 2022 01:20:55 +0100 Subject: [PATCH 327/394] Update nibe to 1.2.1 with support for 2120 pumps (#81824) Update nibe to 1.2.1 --- homeassistant/components/nibe_heatpump/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/manifest.json b/homeassistant/components/nibe_heatpump/manifest.json index 68dc8c7a06c09f..511970832edcaf 100644 --- a/homeassistant/components/nibe_heatpump/manifest.json +++ b/homeassistant/components/nibe_heatpump/manifest.json @@ -3,7 +3,7 @@ "name": "Nibe Heat Pump", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nibe_heatpump", - "requirements": ["nibe==1.2.0"], + "requirements": ["nibe==1.2.1"], "codeowners": ["@elupus"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9946af9a968a24..55fc97cb6d3810 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1163,7 +1163,7 @@ nextcord==2.0.0a8 nextdns==1.1.1 # homeassistant.components.nibe_heatpump -nibe==1.2.0 +nibe==1.2.1 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9df97b9a7aea58..5b7773c71f53a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ nextcord==2.0.0a8 nextdns==1.1.1 # homeassistant.components.nibe_heatpump -nibe==1.2.0 +nibe==1.2.1 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 From dcf68d768e4f628d038f1fdd6e40bad713fbc222 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 9 Nov 2022 00:27:20 +0000 Subject: [PATCH 328/394] [ci skip] Translation update --- .../components/airq/translations/pl.json | 22 ++++++++++++ .../components/arcam_fmj/translations/bg.json | 3 +- .../components/august/translations/bg.json | 1 + .../components/axis/translations/bg.json | 3 +- .../components/braviatv/translations/it.json | 3 ++ .../components/braviatv/translations/pl.json | 3 ++ .../components/daikin/translations/bg.json | 1 + .../components/directv/translations/bg.json | 3 ++ .../components/doorbird/translations/bg.json | 1 + .../components/econet/translations/bg.json | 3 ++ .../components/elkm1/translations/bg.json | 1 + .../components/epson/translations/bg.json | 3 ++ .../forecast_solar/translations/pl.json | 2 +- .../components/foscam/translations/bg.json | 3 ++ .../components/freebox/translations/bg.json | 1 + .../components/generic/translations/it.json | 4 +-- .../homeassistant/translations/bg.json | 6 ++++ .../homeassistant/translations/en.json | 6 ++++ .../homeassistant/translations/es.json | 6 ++++ .../homeassistant/translations/et.json | 6 ++++ .../homeassistant/translations/hu.json | 6 ++++ .../homeassistant/translations/it.json | 6 ++++ .../homeassistant/translations/no.json | 6 ++++ .../homeassistant/translations/pl.json | 6 ++++ .../homeassistant/translations/pt-BR.json | 6 ++++ .../homeassistant/translations/ru.json | 6 ++++ .../homeassistant/translations/zh-Hant.json | 6 ++++ .../components/iaqualink/translations/bg.json | 1 + .../components/ipp/translations/bg.json | 3 ++ .../components/livisi/translations/bg.json | 18 ++++++++++ .../components/livisi/translations/et.json | 18 ++++++++++ .../components/livisi/translations/it.json | 18 ++++++++++ .../components/livisi/translations/no.json | 18 ++++++++++ .../components/livisi/translations/pl.json | 18 ++++++++++ .../livisi/translations/zh-Hant.json | 18 ++++++++++ .../components/mazda/translations/bg.json | 1 + .../components/mqtt/translations/et.json | 6 ++-- .../components/mqtt/translations/it.json | 34 +++++++++---------- .../components/mqtt/translations/no.json | 8 ++--- .../components/mqtt/translations/pl.json | 8 ++--- .../components/mqtt/translations/pt-BR.json | 8 ++--- .../components/mqtt/translations/ru.json | 8 ++--- .../components/mqtt/translations/zh-Hant.json | 10 +++--- .../components/myq/translations/bg.json | 1 + .../components/nexia/translations/bg.json | 1 + .../nibe_heatpump/translations/bg.json | 7 ++++ .../nibe_heatpump/translations/en.json | 12 +++++++ .../nibe_heatpump/translations/es.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/et.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/hu.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/it.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/pl.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/pt-BR.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/ru.json | 34 ++++++++++++++++++- .../components/nuheat/translations/bg.json | 1 + .../components/nuki/translations/bg.json | 1 + .../components/onvif/translations/bg.json | 3 ++ .../components/openuv/translations/es.json | 10 +++++- .../components/openuv/translations/et.json | 10 +++++- .../components/openuv/translations/hu.json | 10 +++++- .../components/openuv/translations/it.json | 10 +++++- .../components/openuv/translations/pt-BR.json | 10 +++++- .../components/openuv/translations/ru.json | 10 +++++- .../panasonic_viera/translations/bg.json | 1 + .../components/powerwall/translations/bg.json | 3 ++ .../components/rachio/translations/bg.json | 3 ++ .../translations/bg.json | 1 + .../somfy_mylink/translations/bg.json | 5 +++ .../synology_dsm/translations/bg.json | 1 + .../totalconnect/translations/bg.json | 3 ++ .../unifiprotect/translations/it.json | 4 +-- .../unifiprotect/translations/pl.json | 6 ++++ .../unifiprotect/translations/ru.json | 4 +-- .../waze_travel_time/translations/bg.json | 3 ++ .../components/zwave_js/translations/bg.json | 1 + 75 files changed, 614 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/airq/translations/pl.json create mode 100644 homeassistant/components/livisi/translations/bg.json create mode 100644 homeassistant/components/livisi/translations/et.json create mode 100644 homeassistant/components/livisi/translations/it.json create mode 100644 homeassistant/components/livisi/translations/no.json create mode 100644 homeassistant/components/livisi/translations/pl.json create mode 100644 homeassistant/components/livisi/translations/zh-Hant.json diff --git a/homeassistant/components/airq/translations/pl.json b/homeassistant/components/airq/translations/pl.json new file mode 100644 index 00000000000000..bf64c7906d97f4 --- /dev/null +++ b/homeassistant/components/airq/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_input": "Nieprawid\u0142owa nazwa hosta lub adres IP" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP", + "password": "Has\u0142o" + }, + "description": "Podaj adres IP lub mDNS urz\u0105dzenia i jego has\u0142o", + "title": "Zidentyfikuj urz\u0105dzenie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/bg.json b/homeassistant/components/arcam_fmj/translations/bg.json index f24b5481b2ce93..60b4c65d0a34eb 100644 --- a/homeassistant/components/arcam_fmj/translations/bg.json +++ b/homeassistant/components/arcam_fmj/translations/bg.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "{host}", "step": { diff --git a/homeassistant/components/august/translations/bg.json b/homeassistant/components/august/translations/bg.json index f2dccb231c100f..7c3899abed7f80 100644 --- a/homeassistant/components/august/translations/bg.json +++ b/homeassistant/components/august/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/axis/translations/bg.json b/homeassistant/components/axis/translations/bg.json index 2cbf383cea86bc..e3ace757b0ec8c 100644 --- a/homeassistant/components/axis/translations/bg.json +++ b/homeassistant/components/axis/translations/bg.json @@ -7,7 +7,8 @@ }, "error": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "Axis \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: {name} ({host})", "step": { diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 7bf9bb98b5a0ab..e17c961fa45393 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Si \u00e8 verificato un errore durante l'aggiornamento dell'elenco delle fonti.\n\nAssicurati che il televisore sia acceso prima di provare a configurarlo." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index adc3a67e603ee8..53847bf7b2c0d8 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "Wyst\u0105pi\u0142 b\u0142\u0105d podczas aktualizowania listy \u017ar\u00f3de\u0142.\n\nUpewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony, zanim spr\u00f3bujesz go skonfigurowa\u0107." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index a07f37ab8d5e45..4ad69dc8249a8b 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -6,6 +6,7 @@ }, "error": { "api_password": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 API \u043a\u043b\u044e\u0447 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/directv/translations/bg.json b/homeassistant/components/directv/translations/bg.json index b43da9ecb18be1..371990a6d32c31 100644 --- a/homeassistant/components/directv/translations/bg.json +++ b/homeassistant/components/directv/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/doorbird/translations/bg.json b/homeassistant/components/doorbird/translations/bg.json index 628eaf62894fc5..02f7ea25f5f2fa 100644 --- a/homeassistant/components/doorbird/translations/bg.json +++ b/homeassistant/components/doorbird/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/econet/translations/bg.json b/homeassistant/components/econet/translations/bg.json index 637413ad06d69d..aeec4d24e19c1a 100644 --- a/homeassistant/components/econet/translations/bg.json +++ b/homeassistant/components/econet/translations/bg.json @@ -4,6 +4,9 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/elkm1/translations/bg.json b/homeassistant/components/elkm1/translations/bg.json index 46a60e96408022..5a7a68927a8cb5 100644 --- a/homeassistant/components/elkm1/translations/bg.json +++ b/homeassistant/components/elkm1/translations/bg.json @@ -6,6 +6,7 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "flow_title": "{mac_address} ({host})", diff --git a/homeassistant/components/epson/translations/bg.json b/homeassistant/components/epson/translations/bg.json index a051d6ca487096..d2c9013bcc5755 100644 --- a/homeassistant/components/epson/translations/bg.json +++ b/homeassistant/components/epson/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/forecast_solar/translations/pl.json b/homeassistant/components/forecast_solar/translations/pl.json index ad01ce4bb549ea..c4be17eed34bb1 100644 --- a/homeassistant/components/forecast_solar/translations/pl.json +++ b/homeassistant/components/forecast_solar/translations/pl.json @@ -25,7 +25,7 @@ "inverter_size": "Rozmiar falownika (Wat)", "modules power": "Ca\u0142kowita moc szczytowa modu\u0142\u00f3w fotowoltaicznych w watach" }, - "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Solar.Forecast. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." + "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Forecast.Solar. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." } } } diff --git a/homeassistant/components/foscam/translations/bg.json b/homeassistant/components/foscam/translations/bg.json index 8e0b1bac05259c..e660bd80f539c9 100644 --- a/homeassistant/components/foscam/translations/bg.json +++ b/homeassistant/components/foscam/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/freebox/translations/bg.json b/homeassistant/components/freebox/translations/bg.json index 9a63019cd8a3b7..dfb1e6f4932689 100644 --- a/homeassistant/components/freebox/translations/bg.json +++ b/homeassistant/components/freebox/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 4e8cdb513cd958..092e1aa45a6cdc 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -50,7 +50,7 @@ "data": { "confirmed_ok": "Questa immagine sembra buona." }, - "description": "![Anteprima immagine fissa fotocamera]({preview_url})", + "description": "![Anteprima immagine fissa della fotocamera]({preview_url})", "title": "Anteprima" } } @@ -79,7 +79,7 @@ "data": { "confirmed_ok": "Questa immagine sembra buona." }, - "description": "![Anteprima immagine fissa fotocamera]({preview_url})", + "description": "![Anteprima immagine fissa della fotocamera]({preview_url})", "title": "Anteprima" }, "content_type": { diff --git a/homeassistant/components/homeassistant/translations/bg.json b/homeassistant/components/homeassistant/translations/bg.json index 260c7bcb57c26f..2c33a7925c2b95 100644 --- a/homeassistant/components/homeassistant/translations/bg.json +++ b/homeassistant/components/homeassistant/translations/bg.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "\u0412\u0430\u043b\u0443\u0442\u0430\u0442\u0430 {currency} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 \u0432\u0430\u043b\u0443\u0442\u0430\u0442\u0430.", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u0432\u0430\u043b\u0443\u0442\u0430 \u0432\u0435\u0447\u0435 \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430" + } + }, "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u043d\u0430 CPU", diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 37c4498b32b403..8008aa813aa227 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "The currency {currency} is no longer in use, please reconfigure the currency configuration.", + "title": "The configured currency is no longer in use" + } + }, "system_health": { "info": { "arch": "CPU Architecture", diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 5b447f7177fad0..e0cb62bee3f70b 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "La moneda {currency} ya no est\u00e1 en uso, por favor, vuelve a configurar la moneda.", + "title": "La moneda configurada ya no est\u00e1 en uso" + } + }, "system_health": { "info": { "arch": "Arquitectura de CPU", diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 7b9b675ed6faa9..e1c765556cf785 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "Valuuta {currency} ei ole enam kasutusel, seadista valuuta uuesti.", + "title": "Seadistatud valuutat enam ei kasutata" + } + }, "system_health": { "info": { "arch": "Protsessori arhitektuur", diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index 7261dfa1f7a883..c7fdde1ca441e5 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "{currency} p\u00e9nznem m\u00e1r nincs haszn\u00e1latban, k\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra a p\u00e9nznemet.", + "title": "A be\u00e1ll\u00edtott p\u00e9nznem m\u00e1r nincs haszn\u00e1latban" + } + }, "system_health": { "info": { "arch": "Processzor architekt\u00fara", diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 432fb9dea46b26..abc7c056692b7c 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "La valuta {currency} non \u00e8 pi\u00f9 in uso, riconfigura la configurazione della valuta.", + "title": "La valuta configurata non \u00e8 pi\u00f9 in uso" + } + }, "system_health": { "info": { "arch": "Architettura della CPU", diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 0932c2e75a69d5..e98f298b5f9ee0 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "Valutaen {currency} er ikke lenger i bruk, vennligst konfigurer valutakonfigurasjonen p\u00e5 nytt.", + "title": "Tollet er ugyldig eller ikke lenger autorisert." + } + }, "system_health": { "info": { "arch": "CPU-arkitektur", diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index bdf26a8b49d611..b40abbe44e7370 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "Waluta {currency} nie jest ju\u017c u\u017cywana. Zmie\u0144 konfiguracj\u0119 waluty.", + "title": "Skonfigurowana waluta nie jest ju\u017c u\u017cywana" + } + }, "system_health": { "info": { "arch": "Architektura procesora", diff --git a/homeassistant/components/homeassistant/translations/pt-BR.json b/homeassistant/components/homeassistant/translations/pt-BR.json index 5ea540d67f341e..3fb4e74054818f 100644 --- a/homeassistant/components/homeassistant/translations/pt-BR.json +++ b/homeassistant/components/homeassistant/translations/pt-BR.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "A moeda {currency} n\u00e3o est\u00e1 mais em uso, reconfigure a configura\u00e7\u00e3o da moeda.", + "title": "A moeda configurada n\u00e3o est\u00e1 mais em uso" + } + }, "system_health": { "info": { "arch": "Arquitetura da CPU", diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json index b0e27b70861f4b..d6326763387310 100644 --- a/homeassistant/components/homeassistant/translations/ru.json +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "\u0412\u0430\u043b\u044e\u0442\u0430 {currency} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0432\u0430\u043b\u044e\u0442\u044b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0430\u044f \u0432\u0430\u043b\u044e\u0442\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f" + } + }, "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index c42acf960c6c9b..dc43dcaf7d3881 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "\u8ca8\u5e63 {currency} \u4e0d\u518d\u4f7f\u7528\u3001\u8acb\u91cd\u65b0\u8a2d\u5b9a\u5176\u4ed6\u8ca8\u5e63\u3002", + "title": "\u8a2d\u5b9a\u8ca8\u5e63\u4e0d\u518d\u4f7f\u7528" + } + }, "system_health": { "info": { "arch": "CPU \u67b6\u69cb", diff --git a/homeassistant/components/iaqualink/translations/bg.json b/homeassistant/components/iaqualink/translations/bg.json index d6a28310dd1cdd..07af4cdf9a370a 100644 --- a/homeassistant/components/iaqualink/translations/bg.json +++ b/homeassistant/components/iaqualink/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/ipp/translations/bg.json b/homeassistant/components/ipp/translations/bg.json index 19680bdcfb4c61..bac35896d948ac 100644 --- a/homeassistant/components/ipp/translations/bg.json +++ b/homeassistant/components/ipp/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/livisi/translations/bg.json b/homeassistant/components/livisi/translations/bg.json new file mode 100644 index 00000000000000..76c9066450f6e9 --- /dev/null +++ b/homeassistant/components/livisi/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "wrong_ip_address": "IP \u0430\u0434\u0440\u0435\u0441\u044a\u0442 \u0435 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u0438\u043b\u0438 SHC \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0434\u043e\u0441\u0442\u0438\u0433\u043d\u0430\u0442 \u043b\u043e\u043a\u0430\u043b\u043d\u043e.", + "wrong_password": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0435 \u0433\u0440\u0435\u0448\u043d\u0430." + }, + "step": { + "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u0438 (\u043b\u043e\u043a\u0430\u043b\u043d\u0430\u0442\u0430) \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/et.json b/homeassistant/components/livisi/translations/et.json new file mode 100644 index 00000000000000..07459d159f40c0 --- /dev/null +++ b/homeassistant/components/livisi/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "wrong_ip_address": "IP-aadress on vale v\u00f5i SHC-d ei ole v\u00f5imalik kohtv\u00f5rgus k\u00e4tte saada.", + "wrong_password": "Salas\u00f5na on vale." + }, + "step": { + "user": { + "data": { + "host": "IP aadress", + "password": "Salas\u00f5na" + }, + "description": "Sisesta SHC IP-aadress ja (kohalik) parool." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/it.json b/homeassistant/components/livisi/translations/it.json new file mode 100644 index 00000000000000..aa39f1037a94fb --- /dev/null +++ b/homeassistant/components/livisi/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "wrong_ip_address": "L'indirizzo IP non \u00e8 corretto o l'SHC non pu\u00f2 essere raggiunto localmente.", + "wrong_password": "La password non \u00e8 corretta." + }, + "step": { + "user": { + "data": { + "host": "Indirizzo IP", + "password": "Password" + }, + "description": "Immettere l'indirizzo IP e la password (locale) dell'SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/no.json b/homeassistant/components/livisi/translations/no.json new file mode 100644 index 00000000000000..a121f5ba163a98 --- /dev/null +++ b/homeassistant/components/livisi/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "wrong_ip_address": "IP-adressen er feil eller SHC kan ikke n\u00e5s lokalt.", + "wrong_password": "Passordet er feil." + }, + "step": { + "user": { + "data": { + "host": "IP adresse", + "password": "Passord" + }, + "description": "Skriv inn IP-adressen og det (lokale) passordet til SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/pl.json b/homeassistant/components/livisi/translations/pl.json new file mode 100644 index 00000000000000..70fd9de4d7d43f --- /dev/null +++ b/homeassistant/components/livisi/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "wrong_ip_address": "Adres IP jest nieprawid\u0142owy lub SHC nie jest dost\u0119pne lokalnie.", + "wrong_password": "Has\u0142o jest nieprawid\u0142owe." + }, + "step": { + "user": { + "data": { + "host": "Adres IP", + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a adres IP i (lokalne) has\u0142o SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/zh-Hant.json b/homeassistant/components/livisi/translations/zh-Hant.json new file mode 100644 index 00000000000000..b1bced211d41b5 --- /dev/null +++ b/homeassistant/components/livisi/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "wrong_ip_address": "IP \u4f4d\u5740\u932f\u8aa4\u6216 SHC \u7121\u6cd5\u900f\u904e\u672c\u5e95\u7aef\u627e\u5230\u88dd\u7f6e\u3002", + "wrong_password": "\u5bc6\u78bc\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "host": "IP \u4f4d\u5740", + "password": "\u5bc6\u78bc" + }, + "description": "\u8f38\u5165 IP \u4f4d\u5740\u53ca\u672c\u5730\u7aef SHC \u5bc6\u78bc\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/bg.json b/homeassistant/components/mazda/translations/bg.json index 1eb89184642acc..2cb991851ca7c4 100644 --- a/homeassistant/components/mazda/translations/bg.json +++ b/homeassistant/components/mazda/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "account_locked": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d. \u041c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 373a8a64a95bd8..47bf2cfd264f0e 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "T\u00e4psemad s\u00e4tted", "broker": "Vahendaja", - "certificate": "Tee kohandatud CA-sertifikaadifaili juurde", - "client_cert": "Kliendi serdifaili tee", + "certificate": "Lae \u00fcles kohandatud CA-sertifikaadi fail", + "client_cert": "Lae \u00fcles kliendi sertifikaadifail", "client_id": "Kliendi ID (juhuslikult genereeritud ID jaoks j\u00e4ta t\u00fchjaks)", - "client_key": "Tee privaatse v\u00f5tme faili juurde", + "client_key": "Lae \u00fcles privaatv\u00f5tme fail", "discovery": "Luba automaatne avastamine", "keepalive": "Aegumiss\u00f5numite saatmise vaheline aeg", "password": "Salas\u00f5na", diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index f8add8256bcd5d..74febaf9caee86 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -10,7 +10,7 @@ "bad_client_cert": "Certificato client non valido, assicurarsi che venga fornito un file codificato PEM", "bad_client_cert_key": "Il certificato del client e il certificato privato non sono una coppia valida", "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password", - "bad_discovery_prefix": "Prefisso di ricerca non valido", + "bad_discovery_prefix": "Prefisso di rilevamento non valido", "bad_will": "Argomento testamento non valido", "cannot_connect": "Impossibile connettersi", "invalid_inclusion": "Il certificato del client e la chiave privata devono essere configurati insieme" @@ -20,15 +20,15 @@ "data": { "advanced_options": "Opzioni avanzate", "broker": "Broker", - "certificate": "Percorso del file del certificato CA personalizzato", - "client_cert": "Percorso per un file di certificato cliente", - "client_id": "ID cliente (lasciare vuoto per generarne uno in modo casuale)", - "client_key": "Percorso per un file della chiave privata", + "certificate": "Carica il file del certificato CA personalizzato", + "client_cert": "Carica il file del certificato client", + "client_id": "ID client (lasciare vuoto per generarne uno casualmente)", + "client_key": "Carica il file della chiave privata", "discovery": "Attiva il rilevamento", - "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento attivit\u00e0", + "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento in attivit\u00e0", "password": "Password", "port": "Porta", - "protocol": "Protocollo MQTT", + "protocol": "protocollo MQTT", "set_ca_cert": "Convalida del certificato del broker", "set_client_cert": "Usa un certificato client", "tls_insecure": "Ignora la convalida del certificato del broker", @@ -38,7 +38,7 @@ }, "hassio_confirm": { "data": { - "discovery": "Attiva il rilevamento" + "discovery": "Abilita il rilevamento" }, "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo: {addon}?", "title": "Broker MQTT tramite il componente aggiuntivo di Home Assistant" @@ -81,13 +81,13 @@ "error": { "bad_birth": "Argomento di nascita non valido", "bad_certificate": "Il certificato CA non \u00e8 valido", - "bad_client_cert": "Certificato cliente non valido, assicurarsi che venga fornito un file con codice PEM", + "bad_client_cert": "Certificato client non valido, assicurarsi che venga fornito un file codificato PEM", "bad_client_cert_key": "Il certificato del client e il certificato privato non sono una coppia valida", "bad_client_key": "Chiave privata non valida, assicurarsi che venga fornito un file codificato PEM senza password", "bad_discovery_prefix": "Prefisso di rilevamento non valido", - "bad_will": "Argomento di testamento non valido", + "bad_will": "Argomento testamento non valido", "cannot_connect": "Impossibile connettersi", - "invalid_inclusion": "Il certificato e la chiave privata del client devono essere configurati insieme" + "invalid_inclusion": "Il certificato del client e la chiave privata devono essere configurati insieme" }, "step": { "broker": { @@ -95,15 +95,15 @@ "advanced_options": "Opzioni avanzate", "broker": "Broker", "certificate": "Carica il file del certificato CA personalizzato", - "client_cert": "Carica il file del certificato cliente", - "client_id": "ID cliente (lasciare vuoto per generarne uno in modo casuale)", + "client_cert": "Carica il file del certificato client", + "client_id": "ID client (lasciare vuoto per generarne uno casualmente)", "client_key": "Carica il file della chiave privata", - "keepalive": "Il tempo che intercorre tra l'invio di messaggi di mantenimento comunicazioni", + "keepalive": "L'intervallo di tempo tra l'invio di messaggi di mantenimento in attivit\u00e0", "password": "Password", "port": "Porta", - "protocol": "Protocollo MQTT", + "protocol": "protocollo MQTT", "set_ca_cert": "Convalida del certificato del broker", - "set_client_cert": "Utilizza un certificato client", + "set_client_cert": "Usa un certificato client", "tls_insecure": "Ignora la convalida del certificato del broker", "username": "Nome utente" }, @@ -117,7 +117,7 @@ "birth_qos": "QoS del messaggio birth", "birth_retain": "Persistenza del messaggio birth", "birth_topic": "Argomento del messaggio birth", - "discovery": "Attiva il rilevamento", + "discovery": "Abilita il rilevamento", "discovery_prefix": "Rileva prefisso", "will_enable": "Abilita il messaggio testamento", "will_payload": "Payload del messaggio testamento", diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 910f47ac02a528..e1f27d29ae9095 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Avanserte instillinger", "broker": "Megler", - "certificate": "Bane til egendefinert CA-sertifikatfil", - "client_cert": "Bane til en klientsertifikatfil", + "certificate": "Last opp egendefinert CA-sertifikatfil", + "client_cert": "Last opp klientsertifikatfil", "client_id": "Klient-ID (la st\u00e5 tomt til tilfeldig generert)", - "client_key": "Bane til en privat n\u00f8kkelfil", + "client_key": "Last opp privat n\u00f8kkelfil", "discovery": "Aktiver oppdagelse", "keepalive": "Tiden mellom sending hold levende meldinger", "password": "Passord", @@ -107,7 +107,7 @@ "tls_insecure": "Ignorer validering av meglersertifikat", "username": "Brukernavn" }, - "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", + "description": "Vennligst fyll ut tilkoblingsinformasjonen for din MQTT megler.", "title": "Megleralternativer" }, "options": { diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 0084c0a1b12c80..33284dcd7c507e 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Opcje zaawansowane", "broker": "Po\u015brednik", - "certificate": "\u015acie\u017cka do pliku z niestandardowym certyfikatem CA", - "client_cert": "\u015acie\u017cka do pliku certyfikatu klienta", + "certificate": "Prze\u015blij plik z niestandardowym certyfikatem CA", + "client_cert": "Prze\u015blij plik certyfikatu klienta", "client_id": "Identyfikator klienta (pozostaw puste, aby wygenerowa\u0107 losowo)", - "client_key": "\u015acie\u017cka do pliku klucza prywatnego", + "client_key": "Prze\u015blij plik klucza prywatnego", "discovery": "W\u0142\u0105cz wykrywanie", "keepalive": "Czas pomi\u0119dzy wys\u0142aniem wiadomo\u015bci \"keep alive\"", "password": "Has\u0142o", @@ -107,7 +107,7 @@ "tls_insecure": "Ignoruj sprawdzanie certyfikatu brokera", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT", + "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT.", "title": "Opcje brokera" }, "options": { diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index d9e8ac43192854..d0fb407331a315 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Op\u00e7\u00f5es avan\u00e7adas", "broker": "Endere\u00e7o do Broker", - "certificate": "Caminho para o arquivo de certificado de CA personalizado", - "client_cert": "Caminho para um arquivo de certificado de cliente", + "certificate": "Carregar arquivo de certificado de CA personalizado", + "client_cert": "Carregar arquivo de certificado do cliente", "client_id": "ID do cliente (deixe em branco para um gerado aleatoriamente)", - "client_key": "Caminho para um arquivo de chave privada", + "client_key": "Carregar arquivo de chave privada", "discovery": "Ativar descoberta", "keepalive": "O tempo entre o envio de mensagens de manuten\u00e7\u00e3o viva", "password": "Senha", @@ -93,7 +93,7 @@ "broker": { "data": { "advanced_options": "Op\u00e7\u00f5es avan\u00e7adas", - "broker": "", + "broker": "Broker", "certificate": "Carregar arquivo de certificado de CA personalizado", "client_cert": "Carregar arquivo de certificado do cliente", "client_id": "ID do cliente (deixe em branco para um gerado aleatoriamente)", diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 069a1066c864a5..7e2fe9ffe55574 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", - "certificate": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0426\u0421", - "client_cert": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", + "certificate": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0426\u0421", + "client_cert": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0430", "client_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c)", - "client_key": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430", + "client_key": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430", "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432", "keepalive": "\u0412\u0440\u0435\u043c\u044f \u043c\u0435\u0436\u0434\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Keep Alive", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", @@ -77,7 +77,7 @@ "error": { "bad_birth": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", "bad_certificate": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0426\u0421 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", - "bad_client_cert": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b, \u0437\u0430\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 PEM.", + "bad_client_cert": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b, \u0437\u0430\u043a\u043e\u0434\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 PEM", "bad_client_cert_key": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043d\u0435 \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043f\u0430\u0440\u043e\u0439.", "bad_client_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0444\u0430\u0439\u043b \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 PEM \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f.", "bad_discovery_prefix": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0442\u043e\u043f\u0438\u043a\u0430 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index eadaebee738f1c..23cd9b3e1099f8 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "\u9032\u968e\u8a2d\u5b9a", "broker": "Broker", - "certificate": "\u81ea\u8a02 CA \u6191\u8b49\u6a94\u6848\u8def\u5f91", - "client_cert": "\u5ba2\u6236\u7aef\u6191\u8b49\u6a94\u6848\u8def\u5f91", + "certificate": "\u4e0a\u50b3\u81ea\u8a02 CA \u6191\u8b49\u6a94\u6848", + "client_cert": "\u4e0a\u50b3\u5ba2\u6236\u7aef\u6191\u8b49\u6a94\u6848", "client_id": "\u5ba2\u6236\u7aef ID (\u4fdd\u6301\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f)", - "client_key": "\u79c1\u9470\u6a94\u6848\u8def\u5f91", + "client_key": "\u4e0a\u50b3\u79c1\u9470\u6a94\u6848", "discovery": "\u958b\u555f\u641c\u5c0b", "keepalive": "\u50b3\u9001\u4fdd\u6301\u6d3b\u52d5\u8a0a\u606f\u9593\u9694\u6642\u9593", "password": "\u5bc6\u78bc", @@ -94,7 +94,7 @@ "data": { "advanced_options": "\u9032\u968e\u8a2d\u5b9a", "broker": "Broker", - "certificate": "\u4e0a\u50b3 CA \u6191\u8b49\u6a94\u6848", + "certificate": "\u4e0a\u50b3\u81ea\u8a02 CA \u6191\u8b49\u6a94\u6848", "client_cert": "\u4e0a\u50b3\u5ba2\u6236\u7aef\u6191\u8b49\u6a94\u6848", "client_id": "\u5ba2\u6236\u7aef ID (\u4fdd\u6301\u7a7a\u767d\u5c07\u81ea\u52d5\u7522\u751f)", "client_key": "\u4e0a\u50b3\u79c1\u9470\u6a94\u6848", @@ -117,7 +117,7 @@ "birth_qos": "Birth \u8a0a\u606f QoS", "birth_retain": "Birth \u8a0a\u606f Retain", "birth_topic": "Birth \u8a0a\u606f\u4e3b\u984c", - "discovery": "\u958b\u555f\u63a2\u7d22", + "discovery": "\u958b\u555f\u641c\u5c0b", "discovery_prefix": "\u63a2\u7d22 prefix", "will_enable": "\u958b\u555f Will \u8a0a\u606f", "will_payload": "Will \u8a0a\u606f payload", diff --git a/homeassistant/components/myq/translations/bg.json b/homeassistant/components/myq/translations/bg.json index 728682f531e847..f175754fdca9fb 100644 --- a/homeassistant/components/myq/translations/bg.json +++ b/homeassistant/components/myq/translations/bg.json @@ -5,6 +5,7 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/nexia/translations/bg.json b/homeassistant/components/nexia/translations/bg.json index 7aa8fb275eab95..5ef9f8721aaf21 100644 --- a/homeassistant/components/nexia/translations/bg.json +++ b/homeassistant/components/nexia/translations/bg.json @@ -4,6 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/nibe_heatpump/translations/bg.json b/homeassistant/components/nibe_heatpump/translations/bg.json index 88f52d84269565..838fde8183119a 100644 --- a/homeassistant/components/nibe_heatpump/translations/bg.json +++ b/homeassistant/components/nibe_heatpump/translations/bg.json @@ -5,6 +5,13 @@ }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "menu_options": { + "nibegw": "NibeGW" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json index 167a8341cd3ec3..a662743c460523 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -41,6 +41,18 @@ "description": "Before attempting to configure the integration, verify that:\n - The NibeGW unit is connected to a heat pump.\n - The MODBUS40 accessory has been enabled in the heat pump configuration.\n - The pump has not gone into an alarm state about missing MODBUS40 accessory." }, "user": { + "data": { + "ip_address": "Remote address", + "listening_port": "Local listening port", + "remote_read_port": "Remote read port", + "remote_write_port": "Remote write port" + }, + "data_description": { + "ip_address": "The address of the NibeGW unit. The device should have been configured with a static address.", + "listening_port": "The local port on this system, that the NibeGW unit is configured to send data to.", + "remote_read_port": "The port the NibeGW unit is listening for read requests on.", + "remote_write_port": "The port the NibeGW unit is listening for write requests on." + }, "description": "Pick the connection method to your pump. In general, F-series pumps require a Nibe GW custom accessory, while an S-series pump has Modbus support built-in.", "menu_options": { "modbus": "Modbus", diff --git a/homeassistant/components/nibe_heatpump/translations/es.json b/homeassistant/components/nibe_heatpump/translations/es.json index 0619471f538d57..0365e60adea16b 100644 --- a/homeassistant/components/nibe_heatpump/translations/es.json +++ b/homeassistant/components/nibe_heatpump/translations/es.json @@ -9,9 +9,37 @@ "model": "El modelo seleccionado no parece ser compatible con modbus40", "read": "Error en la solicitud de lectura de la bomba. Verifica tu `Puerto de lectura remoto` o `Direcci\u00f3n IP remota`.", "unknown": "Error inesperado", + "url": "La URL especificada no es una URL bien formada y compatible", "write": "Error en la solicitud de escritura a la bomba. Verifica tu `Puerto de escritura remoto` o `Direcci\u00f3n IP remota`." }, "step": { + "modbus": { + "data": { + "modbus_unit": "Identificador de unidad Modbus", + "modbus_url": "URL Modbus", + "model": "Modelo de bomba de calor" + }, + "data_description": { + "modbus_unit": "Identificaci\u00f3n de la unidad para su bomba de calor. Por lo general, se puede dejar en 0.", + "modbus_url": "URL Modbus que describe la conexi\u00f3n a tu bomba de calor o unidad MODBUS40. Debe estar en el formato:\n - `tcp://[HOST]:[PUERTO]` para conexi\u00f3n Modbus TCP\n - `serial://[DISPOSITIVO LOCAL]` para una conexi\u00f3n Modbus RTU local\n - `rfc2217://[HOST]:[PUERTO]` para una conexi\u00f3n remota Modbus RTU basada en telnet." + } + }, + "nibegw": { + "data": { + "ip_address": "Direcci\u00f3n remota", + "listening_port": "Puerto de escucha local", + "model": "Modelo de bomba de calor", + "remote_read_port": "Puerto de lectura remoto", + "remote_write_port": "Puerto de escritura remoto" + }, + "data_description": { + "ip_address": "La direcci\u00f3n de la unidad NibeGW. El dispositivo deber\u00eda haber sido configurado con una direcci\u00f3n est\u00e1tica.", + "listening_port": "El puerto local en este sistema, al que la unidad NibeGW est\u00e1 configurada para enviar datos.", + "remote_read_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando peticiones de lectura.", + "remote_write_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando peticiones de escritura." + }, + "description": "Antes de intentar configurar la integraci\u00f3n, verifica que:\n - La unidad NibeGW est\u00e1 conectada a una bomba de calor.\n - Se ha habilitado el accesorio MODBUS40 en la configuraci\u00f3n de la bomba de calor.\n - La bomba no ha entrado en estado de alarma por falta del accesorio MODBUS40." + }, "user": { "data": { "ip_address": "Direcci\u00f3n remota", @@ -25,7 +53,11 @@ "remote_read_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando las peticiones de lectura.", "remote_write_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando peticiones de escritura." }, - "description": "Antes de intentar configurar la integraci\u00f3n, verifica que:\n- La unidad NibeGW est\u00e1 conectada a una bomba de calor.\n- Se ha habilitado el accesorio MODBUS40 en la configuraci\u00f3n de la bomba de calor.\n- La bomba no ha entrado en estado de alarma por falta del accesorio MODBUS40." + "description": "Elige el m\u00e9todo de conexi\u00f3n a tu bomba. En general, las bombas de la serie F requieren un accesorio personalizado Nibe GW, mientras que una bomba de la serie S tiene soporte Modbus incorporado.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/et.json b/homeassistant/components/nibe_heatpump/translations/et.json index 223d0f22c1a86d..8fff7663bb02d1 100644 --- a/homeassistant/components/nibe_heatpump/translations/et.json +++ b/homeassistant/components/nibe_heatpump/translations/et.json @@ -9,9 +9,37 @@ "model": "Valitud mudel ei n\u00e4i toetavat modbus40.", "read": "Viga pumba lugemistaotlusel. Kinnitage oma \"Kaugloetav port\" v\u00f5i \"Kaug-IP-aadress\".", "unknown": "Ootamatu t\u00f5rge", + "url": "M\u00e4\u00e4ratud URL ei ole h\u00e4sti vormindatud ja toetatud URL", "write": "Viga pumba kirjutamise taotlusel. Kontrollige oma `kaugkirjutusport` v\u00f5i `kaug-IP-aadress`." }, "step": { + "modbus": { + "data": { + "modbus_unit": "Modbus-i \u00fcksuse identifikaator", + "modbus_url": "Modbus-i URL", + "model": "Soojuspumba mudel" + }, + "data_description": { + "modbus_unit": "Soojuspumba seadme identifitseerimine. Tavaliselt v\u00f5ib j\u00e4tta 0-le.", + "modbus_url": "Modbusi URL mis kirjeldab \u00fchendust soojuspumba v\u00f5i MODBUS40 seadmega. See peaks olema vormis:\n - `tcp://[HOST]:[PORT]` Modbusi TCP-\u00fchenduse jaoks\n - \"serial://[LOCAL DEVICE]\" kohaliku Modbus RTU \u00fchenduse jaoks\n - `rfc2217://[HOST]:[PORT]` telnetip\u00f5hise Modbus RTU kaug\u00fchenduse jaoks." + } + }, + "nibegw": { + "data": { + "ip_address": "Kaug-IP-aadress", + "listening_port": "Kohalik kuulamisport", + "model": "Soojuspumba mudel", + "remote_read_port": "Kauglugemise port", + "remote_write_port": "Kaugkirjutusport" + }, + "data_description": { + "ip_address": "NibeGW-\u00fcksuse aadress. Seade peaks olema seadistatud staatilise aadressiga.", + "listening_port": "Selle s\u00fcsteemi kohalik port kuhu NibeGW seade on seadistatud andmeid saatma.", + "remote_read_port": "Port, mille kaudu NibeGW-\u00fcksus loeb lugemisp\u00e4ringuid.", + "remote_write_port": "Port, mille kaudu NibeGW-\u00fcksus kuulab kirjutamisp\u00e4ringuid." + }, + "description": "Enne seadistamist veendu, et:\n - NibeGW seade on \u00fchendatud soojuspumbaga.\n - MODBUS40 lisaseade on soojuspumba konfiguratsioonis lubatud.\n - Pump ei ole MODBUS40 lisaseadme puudumise t\u00f5ttu h\u00e4ireolekusse l\u00e4inud." + }, "user": { "data": { "ip_address": "Kaug-IP-aadress", @@ -25,7 +53,11 @@ "remote_read_port": "Port, mille kaudu NibeGW-\u00fcksus loeb lugemisp\u00e4ringuid.", "remote_write_port": "Port, mille kaudu NibeGW-\u00fcksus kuulab kirjutamisp\u00e4ringuid." }, - "description": "Enne sidumise seadistamist veendu, et:\n - NibeGW seade on \u00fchendatud soojuspumbaga.\n - MODBUS40 tarvik on soojuspumba konfiguratsioonis lubatud.\n - Pump ei ole MODBUS40 lisaseadme puudumise t\u00f5ttu h\u00e4ireolekusse l\u00e4inud." + "description": "Vali pumbaga \u00fchendamise viis. \u00dcldiselt vajavad F-seeria pumbad Nibe GW kohandatud tarvikut, S-seeria pumbal on aga sisseehitatud Modbusi tugi.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/hu.json b/homeassistant/components/nibe_heatpump/translations/hu.json index 1dc8ea121796d1..35d9ac5329183f 100644 --- a/homeassistant/components/nibe_heatpump/translations/hu.json +++ b/homeassistant/components/nibe_heatpump/translations/hu.json @@ -9,9 +9,37 @@ "model": "\u00dagy t\u0171nik, hogy a kiv\u00e1lasztott modell nem t\u00e1mogatja a modbus40-et", "read": "Hiba a szivatty\u00fa olvas\u00e1si k\u00e9r\u00e9s\u00e9n\u00e9l. Ellen\u0151rizze a \"T\u00e1voli olvas\u00e1si portot\" vagy a \"T\u00e1voli IP-c\u00edmet\".", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "url": "A megadott URL-c\u00edm nem j\u00f3l form\u00e1zott \u00e9s t\u00e1mogatott URL-c\u00edm", "write": "Hiba a h\u0151szivatty\u00fa \u00edr\u00e1si k\u00e9relm\u00e9ben. Ellen\u0151rizze a portot, c\u00edmet." }, "step": { + "modbus": { + "data": { + "modbus_unit": "Modbus egys\u00e9g azonos\u00edt\u00f3ja", + "modbus_url": "Modbus URL", + "model": "A h\u0151szivatty\u00fa modellje" + }, + "data_description": { + "modbus_unit": "A h\u0151szivatty\u00fa egys\u00e9gazonos\u00edt\u00e1sa. \u00c1ltal\u00e1ban 0 \u00e9rt\u00e9ken hagyhat\u00f3.", + "modbus_url": "Modbus URL, amely le\u00edrja a h\u0151szivatty\u00faval vagy a MODBUS40 egys\u00e9ggel val\u00f3 kapcsolatot. Az \u0171rlapon kell lennie:\n - 'tcp://[HOST]:[PORT]' a Modbus TCP kapcsolathoz\n - \"serial://[HELYI ESZK\u00d6Z]\" helyi Modbus RTU kapcsolathoz\n - 'rfc2217://[HOST]:[PORT]' t\u00e1voli telnet alap\u00fa Modbus RTU kapcsolathoz." + } + }, + "nibegw": { + "data": { + "ip_address": "T\u00e1voli IP-c\u00edm", + "listening_port": "Helyi port", + "model": "A h\u0151szivatty\u00fa modellje", + "remote_read_port": "T\u00e1voli olvas\u00e1si port", + "remote_write_port": "T\u00e1voli \u00edr\u00e1si port" + }, + "data_description": { + "ip_address": "A NibeGW egys\u00e9g c\u00edme. A k\u00e9sz\u00fcl\u00e9ket statikus c\u00edmmel kell konfigur\u00e1lni.", + "listening_port": "A rendszer helyi portja, amelyre a NibeGW egys\u00e9g \u00fagy van konfigur\u00e1lva, hogy adatokat k\u00fcldj\u00f6n.", + "remote_read_port": "A port, amelyen a NibeGW egys\u00e9g olvas\u00e1si k\u00e9r\u00e9seket v\u00e1r.", + "remote_write_port": "A port, amelyen a NibeGW egys\u00e9g az \u00edr\u00e1si k\u00e9r\u00e9seket v\u00e1rja." + }, + "description": "Miel\u0151tt megpr\u00f3b\u00e1ln\u00e1 konfigur\u00e1lni az integr\u00e1ci\u00f3t, ellen\u0151rizze, hogy:\n - A NibeGW egys\u00e9g h\u0151szivatty\u00fahoz van csatlakoztatva.\n - A MODBUS40 kieg\u00e9sz\u00edt\u0151 enged\u00e9lyezve van a h\u0151szivatty\u00fa konfigur\u00e1ci\u00f3j\u00e1ban.\n - A szivatty\u00fa nem l\u00e9pett riaszt\u00e1si \u00e1llapotba a MODBUS40 tartoz\u00e9k hi\u00e1nya miatt." + }, "user": { "data": { "ip_address": "T\u00e1voli IP-c\u00edm", @@ -25,7 +53,11 @@ "remote_read_port": "A port, amelyen a NibeGW egys\u00e9g olvas\u00e1si k\u00e9r\u00e9seket fogad.", "remote_write_port": "A port, amelyen a NibeGW egys\u00e9g \u00edr\u00e1si k\u00e9r\u00e9seket fogad." }, - "description": "Miel\u0151tt megpr\u00f3b\u00e1ln\u00e1 konfigur\u00e1lni az integr\u00e1ci\u00f3t, ellen\u0151rizze, hogy:\n - A NibeGW egys\u00e9g h\u0151szivatty\u00fahoz van csatlakoztatva.\n - A MODBUS40 kieg\u00e9sz\u00edt\u0151 enged\u00e9lyezve van a h\u0151szivatty\u00fa konfigur\u00e1ci\u00f3j\u00e1ban.\n - A szivatty\u00fa nem l\u00e9pett riaszt\u00e1si \u00e1llapotba a MODBUS40 tartoz\u00e9k hi\u00e1nya miatt." + "description": "V\u00e1lassza ki a szivatty\u00fahoz val\u00f3 csatlakoz\u00e1si m\u00f3dot. Az F-sorozat\u00fa szivatty\u00fakhoz \u00e1ltal\u00e1ban Nibe GW egyedi tartoz\u00e9kra van sz\u00fcks\u00e9g, m\u00edg az S-sorozat\u00fa szivatty\u00fak be\u00e9p\u00edtett Modbus-t\u00e1mogat\u00e1ssal rendelkeznek.", + "menu_options": { + "modbus": "ModBUS", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/it.json b/homeassistant/components/nibe_heatpump/translations/it.json index 4884c5c3efc6b7..81da8c2503d6b8 100644 --- a/homeassistant/components/nibe_heatpump/translations/it.json +++ b/homeassistant/components/nibe_heatpump/translations/it.json @@ -9,13 +9,26 @@ "model": "Il modello selezionato non sembra supportare il modbus40", "read": "Errore su richiesta di lettura dalla pompa. Verifica la tua \"Porta di lettura remota\" o \"Indirizzo IP remoto\".", "unknown": "Errore imprevisto", + "url": "L'URL specificato non \u00e8 un URL ben formato e supportato", "write": "Errore nella richiesta di scrittura alla pompa. Verifica la tua \"Porta di scrittura remota\" o \"Indirizzo IP remoto\"." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Identificatore unit\u00e0 Modbus", + "modbus_url": "Modbus URL", + "model": "Modello di pompa di calore" + }, + "data_description": { + "modbus_unit": "Identificazione dell'unit\u00e0 per la tua pompa di calore. Di solito pu\u00f2 essere lasciato a 0.", + "modbus_url": "Modbus URL che descrive la connessione alla pompa di calore o all'unit\u00e0 MODBUS40. Dovrebbe essere nella forma:\n - `tcp://[HOST]:[PORTA]` per la connessione Modbus TCP\n - `serial://[DISPOSITIVO LOCALE]` per una connessione Modbus RTU locale\n - `rfc2217://[HOST]:[PORTA]` per una connessione Modbus RTU remota basata su telnet." + } + }, + "nibegw": { "data": { "ip_address": "Indirizzo remoto", "listening_port": "Porta di ascolto locale", + "model": "Modello di pompa di calore", "remote_read_port": "Porta di lettura remota", "remote_write_port": "Porta di scrittura remota" }, @@ -26,6 +39,25 @@ "remote_write_port": "La porta su cui l'unit\u00e0 NibeGW \u00e8 in ascolto per le richieste di scrittura." }, "description": "Prima di tentare di configurare l'integrazione, verificare che:\n - L'unit\u00e0 NibeGW \u00e8 collegata a una pompa di calore.\n - Nella configurazione della pompa di calore \u00e8 stato abilitato l'accessorio MODBUS40.\n - La pompa non \u00e8 andata in stato di allarme per la mancanza dell'accessorio MODBUS40." + }, + "user": { + "data": { + "ip_address": "Indirizzo remoto", + "listening_port": "Porta di ascolto locale", + "remote_read_port": "Porta di lettura remota", + "remote_write_port": "Porta di scrittura remota" + }, + "data_description": { + "ip_address": "L'indirizzo dell'unit\u00e0 NibeGW. Il dispositivo dovrebbe essere stato configurato con un indirizzo statico.", + "listening_port": "La porta locale su questo sistema a cui l'unit\u00e0 NibeGW \u00e8 configurata per inviare i dati.", + "remote_read_port": "La porta su cui l'unit\u00e0 NibeGW \u00e8 in ascolto per le richieste di lettura.", + "remote_write_port": "La porta su cui l'unit\u00e0 NibeGW \u00e8 in ascolto per le richieste di scrittura." + }, + "description": "Scegli il metodo di connessione alla tua pompa. In generale, le pompe della serie F richiedono un accessorio personalizzato Nibe GW, mentre le pompe della serie S hanno il supporto Modbus integrato.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/pl.json b/homeassistant/components/nibe_heatpump/translations/pl.json index 8298b41c64c9b2..a6cc0785e97032 100644 --- a/homeassistant/components/nibe_heatpump/translations/pl.json +++ b/homeassistant/components/nibe_heatpump/translations/pl.json @@ -9,13 +9,26 @@ "model": "Wybrany model nie obs\u0142uguje modbus40", "read": "B\u0142\u0105d przy \u017c\u0105daniu odczytu z pompy. Sprawd\u017a \u201eZdalny port odczytu\u201d lub \u201eZdalny adres IP\u201d.", "unknown": "Nieoczekiwany b\u0142\u0105d", + "url": "Podany adres URL nie jest poprawnie sformu\u0142owanym i obs\u0142ugiwanym adresem URL", "write": "B\u0142\u0105d przy \u017c\u0105daniu zapisu do pompy. Sprawd\u017a \u201eZdalny port zapisu\u201d lub \u201eZdalny adres IP\u201d." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Identyfikator jednostki Modbus", + "modbus_url": "Adres URL Modbus", + "model": "Model pompy ciep\u0142a" + }, + "data_description": { + "modbus_unit": "Identyfikacja jednostki pompa ciep\u0142a. Zwykle mo\u017cna pozostawi\u0107 0.", + "modbus_url": "Adres URL Modbus opisuj\u0105cy po\u0142\u0105czenie z pomp\u0105 ciep\u0142a lub jednostk\u0105 MODBUS40. Powinien by\u0107 w formacie:\n- `tcp://[HOST]:[PORT]` dla po\u0142\u0105czenia Modbus TCP\n- `serial://[LOCAL DEVICE]` dla lokalnego po\u0142\u0105czenia Modbus RTU\n- `rfc2217://[HOST]:[PORT]` dla zdalnego po\u0142\u0105czenia Modbus RTU opartego na telnet." + } + }, + "nibegw": { "data": { "ip_address": "Zdalny adres IP", "listening_port": "Lokalny port nas\u0142uchiwania", + "model": "Model pompy ciep\u0142a", "remote_read_port": "Zdalny port odczytu", "remote_write_port": "Zdalny port zapisu" }, @@ -26,6 +39,25 @@ "remote_write_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 zapisu." }, "description": "Przed przyst\u0105pieniem do konfiguracji integracji sprawd\u017a, czy:\n - Urz\u0105dzenie NibeGW jest pod\u0142\u0105czona do pompy ciep\u0142a.\n - Akcesorium MODBUS40 zosta\u0142o w\u0142\u0105czone w konfiguracji pompy ciep\u0142a.\n - Pompa nie wesz\u0142a w stan alarmowy z powodu braku akcesorium MODBUS40." + }, + "user": { + "data": { + "ip_address": "Zdalny adres IP", + "listening_port": "Lokalny port nas\u0142uchiwania", + "remote_read_port": "Zdalny port odczytu", + "remote_write_port": "Zdalny port zapisu" + }, + "data_description": { + "ip_address": "Adres urz\u0105dzenia NibeGW. Urz\u0105dzenie powinno by\u0107 skonfigurowane z adresem statycznym.", + "listening_port": "Port lokalny w tym systemie, do kt\u00f3rego urz\u0105dzenie NibeGW jest skonfigurowane do wysy\u0142ania danych.", + "remote_read_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 odczytu.", + "remote_write_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 zapisu." + }, + "description": "Wybierz metod\u0119 po\u0142\u0105czenia z pomp\u0105. Og\u00f3lnie rzecz bior\u0105c, pompy serii F wymagaj\u0105 niestandardowego akcesorium Nibe GW, podczas gdy pompy serii S maj\u0105 wbudowan\u0105 obs\u0142ug\u0119 protoko\u0142u Modbus.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/pt-BR.json b/homeassistant/components/nibe_heatpump/translations/pt-BR.json index 9f99984603641a..1ae021ee11b4b8 100644 --- a/homeassistant/components/nibe_heatpump/translations/pt-BR.json +++ b/homeassistant/components/nibe_heatpump/translations/pt-BR.json @@ -9,9 +9,37 @@ "model": "O modelo selecionado parece n\u00e3o suportar modbus40", "read": "Erro na solicita\u00e7\u00e3o de leitura da bomba. Verifique sua `Porta de leitura remota` ou `Endere\u00e7o IP remoto`.", "unknown": "Erro inesperado", + "url": "A URL especificada n\u00e3o \u00e9 uma URL bem formada e suportada", "write": "Erro na solicita\u00e7\u00e3o de grava\u00e7\u00e3o para bombear. Verifique sua `Porta de grava\u00e7\u00e3o remota` ou `Endere\u00e7o IP remoto`." }, "step": { + "modbus": { + "data": { + "modbus_unit": "Identificador da Unidade Modbus", + "modbus_url": "URL de Modbus", + "model": "Modelo de Bomba de Calor" + }, + "data_description": { + "modbus_unit": "Identifica\u00e7\u00e3o da unidade para a sua Bomba de Calor. Geralmente pode ser deixado em 0.", + "modbus_url": "URL de Modbus que descreve a liga\u00e7\u00e3o \u00e0 sua Bomba de Calor ou unidade MODBUS40. Deve estar no formul\u00e1rio:\n - `tcp://[HOST]:[PORT]` para conex\u00e3o Modbus TCP\n - `serial://[LOCAL DEVICE]` para uma conex\u00e3o Modbus RTU local\n - `rfc2217://[HOST]:[PORT]` para uma conex\u00e3o Modbus RTU baseada em telnet remoto." + } + }, + "nibegw": { + "data": { + "ip_address": "Endere\u00e7o remoto", + "listening_port": "Porta de escuta local", + "model": "Modelo de Bomba de Calor", + "remote_read_port": "Porta de leitura remota", + "remote_write_port": "Porta de grava\u00e7\u00e3o remota" + }, + "data_description": { + "ip_address": "O endere\u00e7o da unidade NibeGW. O dispositivo deve ter sido configurado com um endere\u00e7o est\u00e1tico.", + "listening_port": "A porta local neste sistema para a qual a unidade NibeGW est\u00e1 configurada para enviar dados.", + "remote_read_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de leitura.", + "remote_write_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de grava\u00e7\u00e3o." + }, + "description": "Antes de tentar configurar a integra\u00e7\u00e3o, verifique se:\n - A unidade NibeGW est\u00e1 conectada a uma bomba de calor.\n - O acess\u00f3rio MODBUS40 foi habilitado na configura\u00e7\u00e3o da bomba de calor.\n - A bomba n\u00e3o entrou em estado de alarme por falta de acess\u00f3rio MODBUS40." + }, "user": { "data": { "ip_address": "Endere\u00e7o IP remoto", @@ -25,7 +53,11 @@ "remote_read_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de leitura.", "remote_write_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de grava\u00e7\u00e3o." }, - "description": "Antes de tentar configurar a integra\u00e7\u00e3o, verifique se:\n - A unidade NibeGW est\u00e1 conectada a uma bomba de calor.\n - O acess\u00f3rio MODBUS40 foi habilitado na configura\u00e7\u00e3o da bomba de calor.\n - A bomba n\u00e3o entrou em estado de alarme por falta de acess\u00f3rio MODBUS40." + "description": "Escolha o m\u00e9todo de conex\u00e3o para sua bomba. Em geral, as bombas da s\u00e9rie F requerem um acess\u00f3rio personalizado Nibe GW, enquanto uma bomba da s\u00e9rie S possui suporte Modbus integrado.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/ru.json b/homeassistant/components/nibe_heatpump/translations/ru.json index 59f228b37460b9..42c7a853c94a63 100644 --- a/homeassistant/components/nibe_heatpump/translations/ru.json +++ b/homeassistant/components/nibe_heatpump/translations/ru.json @@ -9,13 +9,26 @@ "model": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 modbus40.", "read": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "url": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0441\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "write": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u043e\u0434\u0443\u043b\u044f Modbus", + "modbus_url": "URL-\u0430\u0434\u0440\u0435\u0441 Modbus", + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430" + }, + "data_description": { + "modbus_unit": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430. \u041e\u0431\u044b\u0447\u043d\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430 0.", + "modbus_url": "URL-\u0430\u0434\u0440\u0435\u0441 Modbus, \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0449\u0438\u0439 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443 \u0438\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 MODBUS40. \u041e\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435:\n- `tcp://[HOST]:[PORT]` \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Modbus TCP\n- `serial://[LOCAL DEVICE]` \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f Modbus RTU\n- `rfc2217://[HOST]:[PORT]` \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Modbus RTU \u0447\u0435\u0440\u0435\u0437 telnet." + } + }, + "nibegw": { "data": { "ip_address": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441", "listening_port": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f", + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430", "remote_read_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f", "remote_write_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438" }, @@ -26,6 +39,25 @@ "remote_write_port": "\u041f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c." }, "description": "\u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u0440\u0438\u0441\u0442\u0443\u043f\u0438\u0442\u044c \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e:\n - \u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443.\n - \u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 MODBUS40 \u0431\u044b\u043b \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0442\u0435\u043f\u043b\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0441\u043e\u0441\u0430.\n - \u041d\u0430\u0441\u043e\u0441 \u043d\u0435 \u043f\u0435\u0440\u0435\u0448\u0435\u043b \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0442\u0440\u0435\u0432\u043e\u0433\u0438 \u0438\u0437 \u0437\u0430 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 MODBUS40." + }, + "user": { + "data": { + "ip_address": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441", + "listening_port": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f", + "remote_read_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f", + "remote_write_port": "\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438" + }, + "data_description": { + "ip_address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 NibeGW. \u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u0441\u043e \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c.", + "listening_port": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0432 \u044d\u0442\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0434\u0430\u043d\u043d\u044b\u0445.", + "remote_read_port": "\u041f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435.", + "remote_write_port": "\u041f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c." + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443. \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0434\u043b\u044f \u043d\u0430\u0441\u043e\u0441\u043e\u0432 \u0441\u0435\u0440\u0438\u0438 F \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 Nibe GW, \u0432 \u0442\u043e \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043d\u0430\u0441\u043e\u0441\u044b \u0441\u0435\u0440\u0438\u0438 S \u0438\u043c\u0435\u044e\u0442 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 Modbus.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nuheat/translations/bg.json b/homeassistant/components/nuheat/translations/bg.json index 03ace4428b140c..47f6792c3b0b7b 100644 --- a/homeassistant/components/nuheat/translations/bg.json +++ b/homeassistant/components/nuheat/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" } diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json index 1a6aff3fe4c6fa..fcb95a304abe9a 100644 --- a/homeassistant/components/nuki/translations/bg.json +++ b/homeassistant/components/nuki/translations/bg.json @@ -4,6 +4,7 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/onvif/translations/bg.json b/homeassistant/components/onvif/translations/bg.json index ba0e0dd6277a95..0fc81f8514f568 100644 --- a/homeassistant/components/onvif/translations/bg.json +++ b/homeassistant/components/onvif/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "configure": { "data": { diff --git a/homeassistant/components/openuv/translations/es.json b/homeassistant/components/openuv/translations/es.json index 66331b8e5a5769..363ccc82ee05c1 100644 --- a/homeassistant/components/openuv/translations/es.json +++ b/homeassistant/components/openuv/translations/es.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "invalid_api_key": "Clave API no v\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "description": "Por favor, vuelve a introducir la clave API para {latitude}, {longitude}.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "api_key": "Clave API", diff --git a/homeassistant/components/openuv/translations/et.json b/homeassistant/components/openuv/translations/et.json index 76145f40ed0e33..4f6fca0ec0103c 100644 --- a/homeassistant/components/openuv/translations/et.json +++ b/homeassistant/components/openuv/translations/et.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_api_key": "Vigane API v\u00f5ti" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Sisesta uuesti API v\u00f5ti {latitude} , {longitude} jaoks.", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index f6493247a99580..6e9a7cc4c1f4df 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "K\u00e9rj\u00fck, adja meg \u00fajra az API-kulcsot a k\u00f6vetkez\u0151h\u00f6z: {latitude}, {longitude}.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/openuv/translations/it.json b/homeassistant/components/openuv/translations/it.json index 4e51b09aec2ae3..f3f4f290017024 100644 --- a/homeassistant/components/openuv/translations/it.json +++ b/homeassistant/components/openuv/translations/it.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + "already_configured": "La posizione \u00e8 gi\u00e0 configurata", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_api_key": "Chiave API non valida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "Inserisci nuovamente la chiave API per {latitude}, {longitude}.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/openuv/translations/pt-BR.json b/homeassistant/components/openuv/translations/pt-BR.json index 9d0c6dd7e8a4aa..c98e76680e7507 100644 --- a/homeassistant/components/openuv/translations/pt-BR.json +++ b/homeassistant/components/openuv/translations/pt-BR.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave API" + }, + "description": "Insira novamente a chave de API para {latitude} , {longitude} .", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/openuv/translations/ru.json b/homeassistant/components/openuv/translations/ru.json index ce7da6af1473cd..179707cf96a3cd 100644 --- a/homeassistant/components/openuv/translations/ru.json +++ b/homeassistant/components/openuv/translations/ru.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API \u0434\u043b\u044f {latitude}, {longitude}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/panasonic_viera/translations/bg.json b/homeassistant/components/panasonic_viera/translations/bg.json index 2ceff63752cb0a..c8dab4b29d9031 100644 --- a/homeassistant/components/panasonic_viera/translations/bg.json +++ b/homeassistant/components/panasonic_viera/translations/bg.json @@ -6,6 +6,7 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_pin_code": "\u0412\u044a\u0432\u0435\u0434\u0435\u043d\u0438\u044f\u0442 \u043e\u0442 \u0412\u0430\u0441 \u041f\u0418\u041d \u043a\u043e\u0434 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d" }, "step": { diff --git a/homeassistant/components/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json index f0092b14bc193e..5660ffe8a10414 100644 --- a/homeassistant/components/powerwall/translations/bg.json +++ b/homeassistant/components/powerwall/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { diff --git a/homeassistant/components/rachio/translations/bg.json b/homeassistant/components/rachio/translations/bg.json index fdbdc5b1cdfa9b..969022c860a52b 100644 --- a/homeassistant/components/rachio/translations/bg.json +++ b/homeassistant/components/rachio/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/bg.json b/homeassistant/components/rituals_perfume_genie/translations/bg.json index 7be659cab0ba4e..51b337ce813198 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/bg.json +++ b/homeassistant/components/rituals_perfume_genie/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json index 5ee98a5d46c80d..fdc3b2a9b4c68d 100644 --- a/homeassistant/components/somfy_mylink/translations/bg.json +++ b/homeassistant/components/somfy_mylink/translations/bg.json @@ -11,5 +11,10 @@ } } } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index dcd0a5ab73015c..d0f8abc40625cf 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -6,6 +6,7 @@ "reconfigure_successful": "\u041f\u0440\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "otp_failed": "\u0414\u0432\u0443\u0441\u0442\u0435\u043f\u0435\u043d\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u0441 \u043d\u043e\u0432 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/totalconnect/translations/bg.json b/homeassistant/components/totalconnect/translations/bg.json index e5aed3bb504d49..70789b8fe099c8 100644 --- a/homeassistant/components/totalconnect/translations/bg.json +++ b/homeassistant/components/totalconnect/translations/bg.json @@ -4,6 +4,9 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, "step": { "locations": { "data": { diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index d44b3fccf31622..fedbef4bdfc4ac 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -43,8 +43,8 @@ }, "issues": { "ea_warning": { - "description": "Stai utilizzando {version} di UniFi Protect. Le versioni di accesso anticipato non sono supportate da Home Assistant e potrebbero causare l'interruzione dell'integrazione di UniFi Protect o non funzionare come previsto.", - "title": "{version} \u00e8 una versione di accesso anticipato di UniFi Protect" + "description": "Si sta utilizzando v{version} di UniFi Protect che \u00e8 una versione in accesso anticipato. Le versioni in accesso anticipato non sono supportate da Home Assistant e potrebbero causare l'interruzione o il mancato funzionamento dell'integrazione di UniFi Protect.", + "title": "UniFi Protect v{version} \u00e8 una versione in accesso anticipato" } }, "options": { diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index a0c4e7eb72c125..22ccc97ca07e41 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -41,6 +41,12 @@ } } }, + "issues": { + "ea_warning": { + "description": "U\u017cywasz UniFi Protect v{version}, kt\u00f3ra jest wersj\u0105 Early Access. Wersje Early Access nie s\u0105 obs\u0142ugiwane przez Home Assistanta i mog\u0105 spowodowa\u0107 uszkodzenie integracji UniFi Protect lub niedzia\u0142anie zgodnie z oczekiwaniami.", + "title": "UniFi Protect v{version} to wersja Early Access" + } + }, "options": { "error": { "invalid_mac_list": "Musi to by\u0107 lista adres\u00f3w MAC oddzielonych przecinkami" diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index 2e8e0687f0a8bc..c90b4eec0f41e4 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -43,8 +43,8 @@ }, "issues": { "ea_warning": { - "description": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 UniFi Protect {version}. \u0412\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f Home Assistant \u0438 \u043c\u043e\u0433\u0443\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", - "title": "UniFi Protect {version} \u2014 \u044d\u0442\u043e \u0432\u0435\u0440\u0441\u0438\u044f \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + "description": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 UniFi Protect v{version}. \u0412\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f Home Assistant \u0438 \u043c\u043e\u0433\u0443\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", + "title": "UniFi Protect v{version} \u2014 \u044d\u0442\u043e \u0432\u0435\u0440\u0441\u0438\u044f \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" } }, "options": { diff --git a/homeassistant/components/waze_travel_time/translations/bg.json b/homeassistant/components/waze_travel_time/translations/bg.json index 5b18b5ba0219c5..295b56e5daabb4 100644 --- a/homeassistant/components/waze_travel_time/translations/bg.json +++ b/homeassistant/components/waze_travel_time/translations/bg.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index 0ed3ce16d2f1c2..73cd79beb627f3 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "not_zwave_js_addon": "\u041e\u0442\u043a\u0440\u0438\u0442\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u043d\u0435 \u0435 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u043d\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u043d\u0430 Z-Wave JS." }, "error": { From ad992f0a8674ff53c52700d8ac45cd372be37210 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 01:09:06 -0600 Subject: [PATCH 329/394] Fix switchbot not becoming available again after unavailable (#81822) * Fix switchbot not becoming available again after unavailable If the advertisment was the same and we were previously marked as unavailable we would not mark the device as available again until the advertisment changed. For lights there is a counter but for the bots there is no counter which means the bots would show unavailable even though they were available again * naming * naming --- .../components/switchbot/coordinator.py | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index ee93c74af3788d..f68c1effc0c566 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -61,6 +61,15 @@ def __init__( self.base_unique_id = base_unique_id self.model = model self._ready_event = asyncio.Event() + self._was_unavailable = True + + @callback + def _async_handle_unavailable( + self, service_info: bluetooth.BluetoothServiceInfoBleak + ) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(service_info) + self._was_unavailable = True @callback def _async_handle_bluetooth_event( @@ -70,16 +79,20 @@ def _async_handle_bluetooth_event( ) -> None: """Handle a Bluetooth event.""" self.ble_device = service_info.device - if adv := switchbot.parse_advertisement_data( - service_info.device, service_info.advertisement + if not ( + adv := switchbot.parse_advertisement_data( + service_info.device, service_info.advertisement + ) ): - if "modelName" in adv.data: - self._ready_event.set() - _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) - if not self.device.advertisement_changed(adv): - return - self.data = flatten_sensors_data(adv.data) - self.device.update_from_advertisement(adv) + return + if "modelName" in adv.data: + self._ready_event.set() + _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) + if not self.device.advertisement_changed(adv) and not self._was_unavailable: + return + self._was_unavailable = False + self.data = flatten_sensors_data(adv.data) + self.device.update_from_advertisement(adv) super()._async_handle_bluetooth_event(service_info, change) async def async_wait_ready(self) -> bool: From 9f691ab3592376863b98ea888fa62a33682d1542 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 9 Nov 2022 09:03:59 +0100 Subject: [PATCH 330/394] Revert "Fix coordinator TypeVar definition (#81298)" (#81834) --- homeassistant/helpers/update_coordinator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index d0d1cb904549c5..768b8040729ecc 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -25,7 +25,7 @@ _T = TypeVar("_T") _DataUpdateCoordinatorT = TypeVar( - "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator" + "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" ) From 059623c6bf04a3f729caff0bedf85231a404e207 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 9 Nov 2022 10:47:10 +0200 Subject: [PATCH 331/394] Remove vestigial move.yml (#81557) --- .github/move.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/move.yml diff --git a/.github/move.yml b/.github/move.yml deleted file mode 100644 index e041083c9ae119..00000000000000 --- a/.github/move.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Configuration for move-issues - https://github.com/dessant/move-issues - -# Delete the command comment. Ignored when the comment also contains other content -deleteCommand: true -# Close the source issue after moving -closeSourceIssue: true -# Lock the source issue after moving -lockSourceIssue: false -# Set custom aliases for targets -# aliases: -# r: repo -# or: owner/repo - From 44e4b8c67693bb467ddf04d72567c216baf12bba Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 9 Nov 2022 11:48:37 +0200 Subject: [PATCH 332/394] Omit native_unit_of_measurement=None (#81844) --- homeassistant/components/bthome/sensor.py | 1 - homeassistant/components/ecobee/sensor.py | 1 - homeassistant/components/growatt_server/sensor_types/mix.py | 1 - homeassistant/components/metoffice/sensor.py | 4 ---- homeassistant/components/ondilo_ico/sensor.py | 1 - 5 files changed, 8 deletions(-) diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 9d68ce2d3b4415..188bd659c6d795 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -174,7 +174,6 @@ (BTHomeSensorDeviceClass.COUNT, None): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.COUNT}", device_class=None, - native_unit_of_measurement=None, state_class=SensorStateClass.MEASUREMENT, ), } diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 9d8793efc295ee..30949e36f8e568 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -76,7 +76,6 @@ class EcobeeSensorEntityDescription( key="airQuality", name="Air Quality Index", device_class=SensorDeviceClass.AQI, - native_unit_of_measurement=None, state_class=SensorStateClass.MEASUREMENT, runtime_key="actualAQScore", ), diff --git a/homeassistant/components/growatt_server/sensor_types/mix.py b/homeassistant/components/growatt_server/sensor_types/mix.py index 6cb61ea2e08fbd..ce29760b317b6b 100644 --- a/homeassistant/components/growatt_server/sensor_types/mix.py +++ b/homeassistant/components/growatt_server/sensor_types/mix.py @@ -230,7 +230,6 @@ key="mix_last_update", name="Last Data Update", api_key="lastdataupdate", - native_unit_of_measurement=None, device_class=SensorDeviceClass.TIMESTAMP, ), # Values from 'dashboard_data' API call diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index ef9643be96ae05..3495f7b7c7a0be 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -51,7 +51,6 @@ key="name", name="Station name", device_class=None, - native_unit_of_measurement=None, icon="mdi:label-outline", entity_registry_enabled_default=False, ), @@ -59,7 +58,6 @@ key="weather", name="Weather", device_class=None, - native_unit_of_measurement=None, icon="mdi:weather-sunny", # but will adapt to current conditions entity_registry_enabled_default=True, ), @@ -91,7 +89,6 @@ SensorEntityDescription( key="wind_direction", name="Wind direction", - native_unit_of_measurement=None, icon="mdi:compass-outline", entity_registry_enabled_default=False, ), @@ -108,7 +105,6 @@ key="visibility", name="Visibility", device_class=None, - native_unit_of_measurement=None, icon="mdi:eye", entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index 85a728ee04af83..b4c6e02879e4c2 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -50,7 +50,6 @@ SensorEntityDescription( key="ph", name="pH", - native_unit_of_measurement=None, icon="mdi:pool", device_class=None, state_class=SensorStateClass.MEASUREMENT, From 21d96e00a244c26b4fa378e9117785566a4f07cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Wed, 9 Nov 2022 10:51:28 +0100 Subject: [PATCH 333/394] Use better icon for system monitor IP sensor (#81779) --- homeassistant/components/systemmonitor/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index eb889264151ca5..d16e2ac3190d6d 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -97,13 +97,13 @@ class SysMonitorSensorEntityDescription(SensorEntityDescription): "ipv4_address": SysMonitorSensorEntityDescription( key="ipv4_address", name="IPv4 address", - icon="mdi:server-network", + icon="mdi:ip-network", mandatory_arg=True, ), "ipv6_address": SysMonitorSensorEntityDescription( key="ipv6_address", name="IPv6 address", - icon="mdi:server-network", + icon="mdi:ip-network", mandatory_arg=True, ), "last_boot": SysMonitorSensorEntityDescription( From 92b5721f8067ad9141737d39b0d9e6eeccaae45e Mon Sep 17 00:00:00 2001 From: Avishay Date: Wed, 9 Nov 2022 12:09:21 +0200 Subject: [PATCH 334/394] Fix modbus hvac mode keys (#81747) Change the HVAC mode register conf constants --- homeassistant/components/modbus/__init__.py | 22 +++++--- homeassistant/components/modbus/climate.py | 22 ++++++-- homeassistant/components/modbus/const.py | 7 +++ tests/components/modbus/test_climate.py | 59 ++++++++++++--------- 4 files changed, 75 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index cda0bb64703b54..d905a5f9423d8f 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, ) -from homeassistant.components.climate import HVACMode from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA as COVER_DEVICE_CLASSES_SCHEMA, ) @@ -66,6 +65,13 @@ CONF_DATA_TYPE, CONF_FANS, CONF_HUB, + CONF_HVAC_MODE_AUTO, + CONF_HVAC_MODE_COOL, + CONF_HVAC_MODE_DRY, + CONF_HVAC_MODE_FAN_ONLY, + CONF_HVAC_MODE_HEAT, + CONF_HVAC_MODE_HEAT_COOL, + CONF_HVAC_MODE_OFF, CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, CONF_HVAC_ONOFF_REGISTER, @@ -227,13 +233,13 @@ { CONF_ADDRESS: cv.positive_int, CONF_HVAC_MODE_VALUES: { - vol.Optional(HVACMode.OFF.value): cv.positive_int, - vol.Optional(HVACMode.HEAT.value): cv.positive_int, - vol.Optional(HVACMode.COOL.value): cv.positive_int, - vol.Optional(HVACMode.HEAT_COOL.value): cv.positive_int, - vol.Optional(HVACMode.AUTO.value): cv.positive_int, - vol.Optional(HVACMode.DRY.value): cv.positive_int, - vol.Optional(HVACMode.FAN_ONLY.value): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_OFF): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_HEAT): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_COOL): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_HEAT_COOL): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_AUTO): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_DRY): cv.positive_int, + vol.Optional(CONF_HVAC_MODE_FAN_ONLY): cv.positive_int, }, } ), diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 92efcfb17d5808..4e6fdf6cae7a3a 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -32,6 +32,13 @@ CALL_TYPE_WRITE_REGISTER, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, + CONF_HVAC_MODE_AUTO, + CONF_HVAC_MODE_COOL, + CONF_HVAC_MODE_DRY, + CONF_HVAC_MODE_FAN_ONLY, + CONF_HVAC_MODE_HEAT, + CONF_HVAC_MODE_HEAT_COOL, + CONF_HVAC_MODE_OFF, CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, CONF_HVAC_ONOFF_REGISTER, @@ -99,10 +106,19 @@ def __init__( self._attr_hvac_mode = None self._hvac_mode_mapping: list[tuple[int, HVACMode]] = [] mode_value_config = mode_config[CONF_HVAC_MODE_VALUES] - for hvac_mode in HVACMode: - if hvac_mode.value in mode_value_config: + + for hvac_mode_kw, hvac_mode in ( + (CONF_HVAC_MODE_OFF, HVACMode.OFF), + (CONF_HVAC_MODE_HEAT, HVACMode.HEAT), + (CONF_HVAC_MODE_COOL, HVACMode.COOL), + (CONF_HVAC_MODE_HEAT_COOL, HVACMode.HEAT_COOL), + (CONF_HVAC_MODE_AUTO, HVACMode.AUTO), + (CONF_HVAC_MODE_DRY, HVACMode.DRY), + (CONF_HVAC_MODE_FAN_ONLY, HVACMode.FAN_ONLY), + ): + if hvac_mode_kw in mode_value_config: self._hvac_mode_mapping.append( - (mode_value_config[hvac_mode.value], hvac_mode) + (mode_value_config[hvac_mode_kw], hvac_mode) ) self._attr_hvac_modes.append(hvac_mode) diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 2ad36f908ce74c..6ed52ae0544ef8 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -56,6 +56,13 @@ CONF_HVAC_MODE_REGISTER = "hvac_mode_register" CONF_HVAC_MODE_VALUES = "values" CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register" +CONF_HVAC_MODE_OFF = "state_off" +CONF_HVAC_MODE_HEAT = "state_heat" +CONF_HVAC_MODE_COOL = "state_cool" +CONF_HVAC_MODE_HEAT_COOL = "state_heat_cool" +CONF_HVAC_MODE_AUTO = "state_auto" +CONF_HVAC_MODE_DRY = "state_dry" +CONF_HVAC_MODE_FAN_ONLY = "state_fan_only" CONF_VERIFY = "verify" CONF_VERIFY_REGISTER = "verify_register" CONF_VERIFY_STATE = "verify_state" diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index e554160d5bb5da..f9e43ae077bcad 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -10,6 +10,13 @@ from homeassistant.components.modbus.const import ( CONF_CLIMATES, CONF_DATA_TYPE, + CONF_HVAC_MODE_AUTO, + CONF_HVAC_MODE_COOL, + CONF_HVAC_MODE_DRY, + CONF_HVAC_MODE_FAN_ONLY, + CONF_HVAC_MODE_HEAT, + CONF_HVAC_MODE_HEAT_COOL, + CONF_HVAC_MODE_OFF, CONF_HVAC_MODE_REGISTER, CONF_HVAC_MODE_VALUES, CONF_HVAC_ONOFF_REGISTER, @@ -82,13 +89,13 @@ CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 11, CONF_HVAC_MODE_VALUES: { - HVACMode.OFF.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.COOL.value: 2, - HVACMode.HEAT_COOL.value: 3, - HVACMode.DRY.value: 4, - HVACMode.FAN_ONLY.value: 5, - HVACMode.AUTO.value: 6, + "state_off": 0, + "state_heat": 1, + "state_cool": 2, + "state_heat_cool": 3, + "state_dry": 4, + "state_fan_only": 5, + "state_auto": 6, }, }, } @@ -114,10 +121,12 @@ async def test_config_climate(hass, mock_modbus): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 11, CONF_HVAC_MODE_VALUES: { - HVACMode.OFF.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.COOL.value: 2, - HVACMode.HEAT_COOL.value: 3, + CONF_HVAC_MODE_OFF: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_COOL: 2, + CONF_HVAC_MODE_HEAT_COOL: 3, + CONF_HVAC_MODE_AUTO: 4, + CONF_HVAC_MODE_FAN_ONLY: 5, }, }, } @@ -132,6 +141,8 @@ async def test_config_hvac_mode_register(hass, mock_modbus): assert HVACMode.HEAT in state.attributes[ATTR_HVAC_MODES] assert HVACMode.COOL in state.attributes[ATTR_HVAC_MODES] assert HVACMode.HEAT_COOL in state.attributes[ATTR_HVAC_MODES] + assert HVACMode.AUTO in state.attributes[ATTR_HVAC_MODES] + assert HVACMode.FAN_ONLY in state.attributes[ATTR_HVAC_MODES] @pytest.mark.parametrize( @@ -203,9 +214,9 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.DRY.value: 2, + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_DRY: 2, }, }, }, @@ -227,9 +238,9 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 0, - HVACMode.HEAT.value: 1, - HVACMode.DRY.value: 2, + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_DRY: 2, }, }, }, @@ -251,9 +262,9 @@ async def test_temperature_climate(hass, expected, mock_do_cycle): CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 0, - HVACMode.HEAT.value: 2, - HVACMode.DRY.value: 3, + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 2, + CONF_HVAC_MODE_DRY: 3, }, }, CONF_HVAC_ONOFF_REGISTER: 119, @@ -374,8 +385,8 @@ async def test_service_climate_set_temperature( CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 1, - HVACMode.HEAT.value: 2, + CONF_HVAC_MODE_COOL: 1, + CONF_HVAC_MODE_HEAT: 2, }, }, } @@ -395,8 +406,8 @@ async def test_service_climate_set_temperature( CONF_HVAC_MODE_REGISTER: { CONF_ADDRESS: 118, CONF_HVAC_MODE_VALUES: { - HVACMode.COOL.value: 1, - HVACMode.HEAT.value: 2, + CONF_HVAC_MODE_COOL: 1, + CONF_HVAC_MODE_HEAT: 2, }, }, CONF_HVAC_ONOFF_REGISTER: 119, From b7f3eb77dcf6d81ad9c0ea6f8ec98803fb9f6d66 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Nov 2022 21:20:27 +1100 Subject: [PATCH 335/394] Add integration_type to usgs_earthquakes_feed (#81846) --- homeassistant/components/usgs_earthquakes_feed/manifest.json | 3 ++- homeassistant/generated/integrations.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index bd8ec9633bdc6c..ee37381a6fa940 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -5,5 +5,6 @@ "requirements": ["aio_geojson_usgs_earthquakes==0.1"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["aio_geojson_usgs_earthquakes"] + "loggers": ["aio_geojson_usgs_earthquakes"], + "integration_type": "service" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 91f19977ccb98c..bbb7928f68f097 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5687,7 +5687,7 @@ }, "usgs_earthquakes_feed": { "name": "U.S. Geological Survey Earthquake Hazards (USGS)", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "cloud_polling" }, From 402bac5ed7af5752baa28b4ee8906ee45a2b50d8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 9 Nov 2022 12:14:26 +0100 Subject: [PATCH 336/394] Improve type hints in camera (#81794) --- homeassistant/components/camera/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index d860776a797078..136cd3b05f1a7b 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -859,8 +859,9 @@ async def websocket_get_prefs( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle request for account info.""" - prefs = hass.data[DATA_CAMERA_PREFS].get(msg["entity_id"]) - connection.send_result(msg["id"], prefs.as_dict()) + prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS] + camera_prefs = prefs.get(msg["entity_id"]) + connection.send_result(msg["id"], camera_prefs.as_dict()) @websocket_api.websocket_command( @@ -876,7 +877,7 @@ async def websocket_update_prefs( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle request for account info.""" - prefs = hass.data[DATA_CAMERA_PREFS] + prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS] changes = dict(msg) changes.pop("id") @@ -955,7 +956,8 @@ async def _async_stream_endpoint_url( ) # Update keepalive setting which manages idle shutdown - camera_prefs = hass.data[DATA_CAMERA_PREFS].get(camera.entity_id) + prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS] + camera_prefs = prefs.get(camera.entity_id) stream.keepalive = camera_prefs.preload_stream stream.orientation = camera_prefs.orientation From 738419309d3bdb1d60835426cc029194b3767e14 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 9 Nov 2022 13:22:14 +0200 Subject: [PATCH 337/394] Add numpy requirement to stream (#81841) Closes https://github.com/home-assistant/core/pull/81790 --- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index 1f79da20542544..815acd5b39c47c 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0"], + "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0", "numpy==1.23.2"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 55fc97cb6d3810..c9b0fa1e77d11b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1195,6 +1195,7 @@ numato-gpio==0.10.0 # homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv +# homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend numpy==1.23.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b7773c71f53a9..662cdd1ad4cd3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -870,6 +870,7 @@ numato-gpio==0.10.0 # homeassistant.components.compensation # homeassistant.components.iqvia # homeassistant.components.opencv +# homeassistant.components.stream # homeassistant.components.tensorflow # homeassistant.components.trend numpy==1.23.2 From 2eb37f527a898692a0b2ebad9815398356eeffb8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 9 Nov 2022 12:45:33 +0100 Subject: [PATCH 338/394] Update psutil to 5.9.4 (#81840) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index a2db68f11c7441..a231c3e83f3789 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.9.3"], + "requirements": ["psutil==5.9.4"], "codeowners": [], "iot_class": "local_push", "loggers": ["psutil"] diff --git a/requirements_all.txt b/requirements_all.txt index c9b0fa1e77d11b..612737c03a3abc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1362,7 +1362,7 @@ proxmoxer==1.3.1 psutil-home-assistant==0.0.1 # homeassistant.components.systemmonitor -psutil==5.9.3 +psutil==5.9.4 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 From 0cd9fe3288b4f4a3f027c7462f1896617e3aaeee Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Wed, 9 Nov 2022 13:45:28 +0100 Subject: [PATCH 339/394] Landis+Gyr Heat Meter code improvements (#81184) --- .../landisgyr_heat_meter/__init__.py | 44 +++++++-- .../landisgyr_heat_meter/config_flow.py | 79 ++++++++------- .../components/landisgyr_heat_meter/const.py | 45 +++++---- .../landisgyr_heat_meter/manifest.json | 1 + .../components/landisgyr_heat_meter/sensor.py | 33 ++----- .../landisgyr_heat_meter/strings.json | 4 - .../landisgyr_heat_meter/test_config_flow.py | 96 ++++++------------- .../landisgyr_heat_meter/test_init.py | 70 ++++++++++++-- .../landisgyr_heat_meter/test_sensor.py | 6 +- 9 files changed, 214 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 3ef235ff8af924..34724c07ca9b94 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -4,11 +4,12 @@ from datetime import timedelta import logging -from ultraheat_api import HeatMeterService, UltraheatReader +import ultraheat_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_registry import async_migrate_entries from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -22,13 +23,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up heat meter from a config entry.""" _LOGGER.debug("Initializing %s integration on %s", DOMAIN, entry.data[CONF_DEVICE]) - reader = UltraheatReader(entry.data[CONF_DEVICE]) - - api = HeatMeterService(reader) + reader = ultraheat_api.UltraheatReader(entry.data[CONF_DEVICE]) + api = ultraheat_api.HeatMeterService(reader) async def async_update_data(): """Fetch data from the API.""" - _LOGGER.info("Polling on %s", entry.data[CONF_DEVICE]) + _LOGGER.debug("Polling on %s", entry.data[CONF_DEVICE]) return await hass.async_add_executor_job(api.read) # Polling is only daily to prevent battery drain. @@ -53,3 +53,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + + # Removing domain name and config entry id from entity unique id's, replacing it with device number + if config_entry.version == 1: + + config_entry.version = 2 + + device_number = config_entry.data["device_number"] + + @callback + def update_entity_unique_id(entity_entry): + """Update unique ID of entity entry.""" + if entity_entry.platform in entity_entry.unique_id: + return { + "new_unique_id": entity_entry.unique_id.replace( + f"{entity_entry.platform}_{entity_entry.config_entry_id}", + f"{device_number}", + ) + } + + await async_migrate_entries( + hass, config_entry.entry_id, update_entity_unique_id + ) + hass.config_entries.async_update_entry(config_entry) + + _LOGGER.info("Migration to version %s successful", config_entry.version) + + return True diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py index 2e244a9a65f53e..f12992166fb77c 100644 --- a/homeassistant/components/landisgyr_heat_meter/config_flow.py +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -1,17 +1,21 @@ """Config flow for Landis+Gyr Heat Meter integration.""" from __future__ import annotations +import asyncio import logging -import os +from typing import Any import async_timeout import serial -import serial.tools.list_ports -from ultraheat_api import HeatMeterService, UltraheatReader +from serial.tools import list_ports +import ultraheat_api import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import usb from homeassistant.const import CONF_DEVICE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN, ULTRAHEAT_TIMEOUT @@ -30,9 +34,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Ultraheat Heat Meter.""" - VERSION = 1 + VERSION = 2 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Step when setting up serial configuration.""" errors = {} @@ -41,7 +47,7 @@ async def async_step_user(self, user_input=None): return await self.async_step_setup_serial_manual_path() dev_path = await self.hass.async_add_executor_job( - get_serial_by_id, user_input[CONF_DEVICE] + usb.get_serial_by_id, user_input[CONF_DEVICE] ) _LOGGER.debug("Using this path : %s", dev_path) @@ -50,12 +56,15 @@ async def async_step_user(self, user_input=None): except CannotConnect: errors["base"] = "cannot_connect" - ports = await self.get_ports() + ports = await get_usb_ports(self.hass) + ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(ports)}) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_setup_serial_manual_path(self, user_input=None): + async def async_step_setup_serial_manual_path( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Set path manually.""" errors = {} @@ -78,7 +87,7 @@ async def validate_and_create_entry(self, dev_path): model, device_number = await self.validate_ultraheat(dev_path) _LOGGER.debug("Got model %s and device_number %s", model, device_number) - await self.async_set_unique_id(device_number) + await self.async_set_unique_id(f"{device_number}") self._abort_if_unique_id_configured() data = { CONF_DEVICE: dev_path, @@ -90,48 +99,44 @@ async def validate_and_create_entry(self, dev_path): data=data, ) - async def validate_ultraheat(self, port: str): + async def validate_ultraheat(self, port: str) -> tuple[str, str]: """Validate the user input allows us to connect.""" - reader = UltraheatReader(port) - heat_meter = HeatMeterService(reader) + reader = ultraheat_api.UltraheatReader(port) + heat_meter = ultraheat_api.HeatMeterService(reader) try: async with async_timeout.timeout(ULTRAHEAT_TIMEOUT): # validate and retrieve the model and device number for a unique id data = await self.hass.async_add_executor_job(heat_meter.read) - _LOGGER.debug("Got data from Ultraheat API: %s", data) - except Exception as err: + except (asyncio.TimeoutError, serial.serialutil.SerialException) as err: _LOGGER.warning("Failed read data from: %s. %s", port, err) raise CannotConnect(f"Error communicating with device: {err}") from err - _LOGGER.debug("Successfully connected to %s", port) + _LOGGER.debug("Successfully connected to %s. Got data: %s", port, data) return data.model, data.device_number - async def get_ports(self) -> dict: - """Get the available ports.""" - ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) - formatted_ports = {} - for port in ports: - formatted_ports[ - port.device - ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( - f" - {port.manufacturer}" if port.manufacturer else "" - ) - formatted_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH - return formatted_ports - -def get_serial_by_id(dev_path: str) -> str: - """Return a /dev/serial/by-id match for given device if available.""" - by_id = "/dev/serial/by-id" - if not os.path.isdir(by_id): - return dev_path +async def get_usb_ports(hass: HomeAssistant) -> dict[str, str]: + """Return a dict of USB ports and their friendly names.""" + ports = await hass.async_add_executor_job(list_ports.comports) + port_descriptions = {} + for port in ports: + # this prevents an issue with usb_device_from_port not working for ports without vid on RPi + if port.vid: + usb_device = usb.usb_device_from_port(port) + dev_path = usb.get_serial_by_id(usb_device.device) + human_name = usb.human_readable_device_name( + dev_path, + usb_device.serial_number, + usb_device.manufacturer, + usb_device.description, + usb_device.vid, + usb_device.pid, + ) + port_descriptions[dev_path] = human_name - for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): - if os.path.realpath(path) == dev_path: - return path - return dev_path + return port_descriptions class CannotConnect(HomeAssistantError): diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py index 57a8f9d9be4bef..7767a491f3b3a1 100644 --- a/homeassistant/components/landisgyr_heat_meter/const.py +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -5,7 +5,15 @@ SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import ENERGY_MEGA_WATT_HOUR, TEMP_CELSIUS, VOLUME_CUBIC_METERS +from homeassistant.const import ( + ENERGY_MEGA_WATT_HOUR, + POWER_KILO_WATT, + TEMP_CELSIUS, + TIME_HOURS, + TIME_MINUTES, + VOLUME_CUBIC_METERS, + VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, +) from homeassistant.helpers.entity import EntityCategory DOMAIN = "landisgyr_heat_meter" @@ -26,6 +34,7 @@ key="volume_usage_m3", icon="mdi:fire", name="Volume usage", + device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=VOLUME_CUBIC_METERS, state_class=SensorStateClass.TOTAL, ), @@ -56,12 +65,14 @@ key="volume_previous_year_m3", icon="mdi:fire", name="Volume usage previous year", + device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=VOLUME_CUBIC_METERS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="ownership_number", name="Ownership number", + icon="mdi:identifier", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -73,41 +84,41 @@ SensorEntityDescription( key="device_number", name="Device number", + icon="mdi:identifier", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="measurement_period_minutes", name="Measurement period minutes", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_MINUTES, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="power_max_kw", name="Power max", - native_unit_of_measurement="kW", - icon="mdi:power-plug-outline", + native_unit_of_measurement=POWER_KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="power_max_previous_year_kw", name="Power max previous year", - native_unit_of_measurement="kW", - icon="mdi:power-plug-outline", + native_unit_of_measurement=POWER_KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="flowrate_max_m3ph", name="Flowrate max", - native_unit_of_measurement="m3ph", + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="flowrate_max_previous_year_m3ph", name="Flowrate max previous year", - native_unit_of_measurement="m3ph", + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -115,7 +126,6 @@ key="return_temperature_max_c", name="Return temperature max", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -123,7 +133,6 @@ key="return_temperature_max_previous_year_c", name="Return temperature max previous year", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -131,7 +140,6 @@ key="flow_temperature_max_c", name="Flow temperature max", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -139,32 +147,35 @@ key="flow_temperature_max_previous_year_c", name="Flow temperature max previous year", native_unit_of_measurement=TEMP_CELSIUS, - icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="operating_hours", name="Operating hours", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="flow_hours", name="Flow hours", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="fault_hours", name="Fault hours", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="fault_hours_previous_year", name="Fault hours previous year", - icon="mdi:clock-outline", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -189,7 +200,7 @@ SensorEntityDescription( key="measuring_range_m3ph", name="Measuring range", - native_unit_of_measurement="m3ph", + native_unit_of_measurement=VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index 7be3115a6d3697..a20225c88b0855 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -9,5 +9,6 @@ "homekit": {}, "dependencies": [], "codeowners": ["@vpathuis"], + "dependencies": ["usb"], "iot_class": "local_polling" } diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 23a6e217458cf3..2b4fc6edea8121 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -4,13 +4,8 @@ from dataclasses import asdict import logging -from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, - RestoreSensor, - SensorDeviceClass, -) +from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,8 +22,6 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensor platform.""" - _LOGGER.info("The Landis+Gyr Heat Meter sensor platform is being set up!") - unique_id = entry.entry_id coordinator = hass.data[DOMAIN][entry.entry_id] @@ -44,7 +37,7 @@ async def async_setup_entry( sensors = [] for description in HEAT_METER_SENSOR_TYPES: - sensors.append(HeatMeterSensor(coordinator, unique_id, description, device)) + sensors.append(HeatMeterSensor(coordinator, description, device)) async_add_entities(sensors) @@ -52,24 +45,16 @@ async def async_setup_entry( class HeatMeterSensor(CoordinatorEntity, RestoreSensor): """Representation of a Sensor.""" - def __init__(self, coordinator, unique_id, description, device): + def __init__(self, coordinator, description, device): """Set up the sensor with the initial values.""" super().__init__(coordinator) self.key = description.key - self._attr_unique_id = f"{DOMAIN}_{unique_id}_{description.key}" - self._attr_name = "Heat Meter " + description.name - if hasattr(description, "icon"): - self._attr_icon = description.icon - if hasattr(description, "entity_category"): - self._attr_entity_category = description.entity_category - if hasattr(description, ATTR_STATE_CLASS): - self._attr_state_class = description.state_class - if hasattr(description, ATTR_DEVICE_CLASS): - self._attr_device_class = description.device_class - if hasattr(description, ATTR_UNIT_OF_MEASUREMENT): - self._attr_native_unit_of_measurement = ( - description.native_unit_of_measurement - ) + self._attr_unique_id = ( + f"{coordinator.config_entry.data['device_number']}_{description.key}" + ) + self._attr_name = f"Heat Meter {description.name}" + self.entity_description = description + self._attr_device_info = device self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year")) diff --git a/homeassistant/components/landisgyr_heat_meter/strings.json b/homeassistant/components/landisgyr_heat_meter/strings.json index 61e170af2b3cac..4bae2490006fe9 100644 --- a/homeassistant/components/landisgyr_heat_meter/strings.json +++ b/homeassistant/components/landisgyr_heat_meter/strings.json @@ -12,10 +12,6 @@ } } }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py index 9200a9b3d23dda..576388686477c0 100644 --- a/tests/components/landisgyr_heat_meter/test_config_flow.py +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -1,7 +1,8 @@ """Test the Landis + Gyr Heat Meter config flow.""" from dataclasses import dataclass -from unittest.mock import MagicMock, patch +from unittest.mock import patch +import serial import serial.tools.list_ports from homeassistant import config_entries @@ -9,6 +10,10 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + +API_HEAT_METER_SERVICE = "homeassistant.components.landisgyr_heat_meter.config_flow.ultraheat_api.HeatMeterService" + def mock_serial_port(): """Mock of a serial port.""" @@ -17,6 +22,8 @@ def mock_serial_port(): port.manufacturer = "Virtual serial port" port.device = "/dev/ttyUSB1234" port.description = "Some serial port" + port.pid = 9876 + port.vid = 5678 return port @@ -29,7 +36,7 @@ class MockUltraheatRead: device_number: str -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: """Test manual entry.""" @@ -67,7 +74,7 @@ async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: } -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) @patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: """Test select from list entry.""" @@ -94,11 +101,11 @@ async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> No } -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: """Test manual entry fails.""" - mock_heat_meter().read.side_effect = Exception + mock_heat_meter().read.side_effect = serial.serialutil.SerialException result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -128,12 +135,12 @@ async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) @patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: """Test select from list entry fails.""" - mock_heat_meter().read.side_effect = Exception + mock_heat_meter().read.side_effect = serial.serialutil.SerialException port = mock_serial_port() result = await hass.config_entries.flow.async_init( @@ -151,77 +158,36 @@ async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) assert result["errors"] == {"base": "cannot_connect"} -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch(API_HEAT_METER_SERVICE) @patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) -async def test_get_serial_by_id_realpath( +async def test_already_configured( mock_port, mock_heat_meter, hass: HomeAssistant ) -> None: - """Test getting the serial path name.""" - - mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") - port = mock_serial_port() + """Test we abort if the Heat Meter is already configured.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - - scandir = [MagicMock(), MagicMock()] - scandir[0].path = "/dev/ttyUSB1234" - scandir[0].is_symlink.return_value = True - scandir[1].path = "/dev/ttyUSB5678" - scandir[1].is_symlink.return_value = True - - with patch("os.path") as path: - with patch("os.scandir", return_value=scandir): - path.isdir.return_value = True - path.realpath.side_effect = ["/dev/ttyUSB1234", "/dev/ttyUSB5678"] - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device": port.device} - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "LUGCUH50" - assert result["data"] == { - "device": port.device, + # create and add existing entry + entry_data = { + "device": "/dev/USB0", "model": "LUGCUH50", "device_number": "123456789", } + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="123456789", data=entry_data) + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() -@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") -@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) -async def test_get_serial_by_id_dev_path( - mock_port, mock_heat_meter, hass: HomeAssistant -) -> None: - """Test getting the serial path name with no realpath result.""" - + # run flow and see if it aborts mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") port = mock_serial_port() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} - scandir = [MagicMock()] - scandir[0].path.return_value = "/dev/serial/by-id/USB5678" - scandir[0].is_symlink.return_value = True - - with patch("os.path") as path: - with patch("os.scandir", return_value=scandir): - path.isdir.return_value = True - path.realpath.side_effect = ["/dev/ttyUSB5678"] - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {"device": port.device} - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "LUGCUH50" - assert result["data"] == { - "device": port.device, - "model": "LUGCUH50", - "device_number": "123456789", - } + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/landisgyr_heat_meter/test_init.py b/tests/components/landisgyr_heat_meter/test_init.py index b3630fc48728c6..6e300ec1332069 100644 --- a/tests/components/landisgyr_heat_meter/test_init.py +++ b/tests/components/landisgyr_heat_meter/test_init.py @@ -1,22 +1,78 @@ """Test the Landis + Gyr Heat Meter init.""" -from homeassistant.const import CONF_DEVICE +from unittest.mock import patch + +from homeassistant.components.landisgyr_heat_meter.const import ( + DOMAIN as LANDISGYR_HEAT_METER_DOMAIN, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry +API_HEAT_METER_SERVICE = ( + "homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService" +) + -async def test_unload_entry(hass): +@patch(API_HEAT_METER_SERVICE) +async def test_unload_entry(_, hass): """Test removing config entry.""" - entry = MockConfigEntry( + mock_entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "12345", + } + mock_entry = MockConfigEntry( + domain="landisgyr_heat_meter", + title="LUGCUH50", + entry_id="987654321", + data=mock_entry_data, + ) + mock_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + assert "landisgyr_heat_meter" in hass.config.components + + assert await hass.config_entries.async_remove(mock_entry.entry_id) + + +@patch(API_HEAT_METER_SERVICE) +async def test_migrate_entry(_, hass): + """Test successful migration of entry data from version 1 to 2.""" + + mock_entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "12345", + } + mock_entry = MockConfigEntry( domain="landisgyr_heat_meter", title="LUGCUH50", - data={CONF_DEVICE: "/dev/1234"}, + entry_id="987654321", + data=mock_entry_data, ) + assert mock_entry.data == mock_entry_data + assert mock_entry.version == 1 + + mock_entry.add_to_hass(hass) - entry.add_to_hass(hass) + # Create entity entry to migrate to new unique ID + registry = er.async_get(hass) + registry.async_get_or_create( + SENSOR_DOMAIN, + LANDISGYR_HEAT_METER_DOMAIN, + "landisgyr_heat_meter_987654321_measuring_range_m3ph", + suggested_object_id="heat_meter_measuring_range", + config_entry=mock_entry, + ) - assert await hass.config_entries.async_setup(entry.entry_id) + assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() assert "landisgyr_heat_meter" in hass.config.components - assert await hass.config_entries.async_remove(entry.entry_id) + # Check if entity unique id is migrated successfully + assert mock_entry.version == 2 + entity = registry.async_get("sensor.heat_meter_measuring_range") + assert entity.unique_id == "12345_measuring_range_m3ph" diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index 1a068093d0ee26..cbaca71e52f834 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -42,7 +42,7 @@ class MockHeatMeterResponse: meter_date_time: datetime.datetime -@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService") async def test_create_sensors(mock_heat_meter, hass): """Test sensor.""" entry_data = { @@ -107,7 +107,7 @@ async def test_create_sensors(mock_heat_meter, hass): assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC -@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +@patch("homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService") async def test_restore_state(mock_heat_meter, hass): """Test sensor restore state.""" # Home assistant is not running yet @@ -177,7 +177,6 @@ async def test_restore_state(mock_heat_meter, hass): mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) - await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() # restore from cache @@ -195,6 +194,5 @@ async def test_restore_state(mock_heat_meter, hass): state = hass.states.get("sensor.heat_meter_device_number") assert state - print("STATE IS: ", state) assert state.state == "devicenr_789" assert state.attributes.get(ATTR_STATE_CLASS) is None From 4b4bf54994a3d164ce38fd4b014bc4c2b9cd3a86 Mon Sep 17 00:00:00 2001 From: Hessel Date: Wed, 9 Nov 2022 15:22:54 +0100 Subject: [PATCH 340/394] Bump wallbox to 0.4.12 (#81852) Version Bump for WALLBOX --- homeassistant/components/wallbox/__init__.py | 6 +++++- homeassistant/components/wallbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 6382cf05940086..d927e7282edfc2 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -194,7 +194,11 @@ async def async_pause_charger(self, pause: bool) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Wallbox from a config entry.""" - wallbox = Wallbox(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) + wallbox = Wallbox( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + jwtTokenDrift=UPDATE_INTERVAL, + ) wallbox_coordinator = WallboxCoordinator( entry.data[CONF_STATION], wallbox, diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 433a759bea59a3..e1d64ab9478413 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -3,7 +3,7 @@ "name": "Wallbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", - "requirements": ["wallbox==0.4.10"], + "requirements": ["wallbox==0.4.12"], "codeowners": ["@hesselonline"], "iot_class": "cloud_polling", "loggers": ["wallbox"] diff --git a/requirements_all.txt b/requirements_all.txt index 612737c03a3abc..d8dab65484262c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2529,7 +2529,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.10 +wallbox==0.4.12 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 662cdd1ad4cd3c..041b857733196f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1757,7 +1757,7 @@ vultr==0.1.2 wakeonlan==2.1.0 # homeassistant.components.wallbox -wallbox==0.4.10 +wallbox==0.4.12 # homeassistant.components.folder_watcher watchdog==2.1.9 From b72876d369e4320ab5114f70ba75d4000c28add1 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Wed, 9 Nov 2022 15:31:58 +0100 Subject: [PATCH 341/394] Add support for BTHome V2 to bthome (#81811) * Add BTHome v2 support * Add new sensor types * Add new sensor types --- homeassistant/components/bthome/manifest.json | 6 +- homeassistant/components/bthome/sensor.py | 88 ++- homeassistant/generated/bluetooth.py | 5 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bthome/__init__.py | 23 +- tests/components/bthome/test_binary_sensor.py | 98 ++- tests/components/bthome/test_sensor.py | 610 +++++++++++++++++- 8 files changed, 790 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 3b4cbe2f4f468b..0111f765014721 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -11,9 +11,13 @@ { "connectable": false, "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb" + }, + { + "connectable": false, + "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==1.2.2"], + "requirements": ["bthome-ble==2.2.1"], "dependencies": ["bluetooth"], "codeowners": ["@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 188bd659c6d795..e7757c2e8723fe 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -21,16 +21,20 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, + DEGREE, + ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, LIGHT_LUX, - MASS_KILOGRAMS, - MASS_POUNDS, PERCENTAGE, - POWER_WATT, - PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, + TIME_SECONDS, + UnitOfEnergy, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory @@ -43,7 +47,7 @@ (BTHomeSensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), (BTHomeSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( @@ -61,7 +65,7 @@ (BTHomeSensorDeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, - native_unit_of_measurement=PRESSURE_MBAR, + native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, ), (BTHomeSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( @@ -86,13 +90,13 @@ ): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.ENERGY}_{Units.ENERGY_KILO_WATT_HOUR}", device_class=SensorDeviceClass.ENERGY, - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, ), (BTHomeSensorDeviceClass.POWER, Units.POWER_WATT): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.POWER}_{Units.POWER_WATT}", device_class=SensorDeviceClass.POWER, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, ), ( @@ -146,14 +150,14 @@ (BTHomeSensorDeviceClass.MASS, Units.MASS_KILOGRAMS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_KILOGRAMS}", device_class=SensorDeviceClass.WEIGHT, - native_unit_of_measurement=MASS_KILOGRAMS, + native_unit_of_measurement=UnitOfMass.KILOGRAMS, state_class=SensorStateClass.MEASUREMENT, ), # Used for mass sensor with lb unit (BTHomeSensorDeviceClass.MASS, Units.MASS_POUNDS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.MASS}_{Units.MASS_POUNDS}", device_class=SensorDeviceClass.WEIGHT, - native_unit_of_measurement=MASS_POUNDS, + native_unit_of_measurement=UnitOfMass.POUNDS, state_class=SensorStateClass.MEASUREMENT, ), # Used for moisture sensor @@ -167,7 +171,7 @@ (BTHomeSensorDeviceClass.DEW_POINT, Units.TEMP_CELSIUS): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), # Used for count sensor @@ -176,6 +180,64 @@ device_class=None, state_class=SensorStateClass.MEASUREMENT, ), + # Used for rotation sensor + (BTHomeSensorDeviceClass.ROTATION, Units.DEGREE): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.ROTATION}_{Units.DEGREE}", + device_class=None, + native_unit_of_measurement=DEGREE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for distance sensor in mm + ( + BTHomeSensorDeviceClass.DISTANCE, + Units.LENGTH_MILLIMETERS, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.DISTANCE}_{Units.LENGTH_MILLIMETERS}", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for distance sensor in m + (BTHomeSensorDeviceClass.DISTANCE, Units.LENGTH_METERS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.DISTANCE}_{Units.LENGTH_METERS}", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.METERS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for duration sensor + (BTHomeSensorDeviceClass.DURATION, Units.TIME_SECONDS): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.DURATION}_{Units.TIME_SECONDS}", + device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_SECONDS, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for current sensor + ( + BTHomeSensorDeviceClass.CURRENT, + Units.ELECTRIC_CURRENT_AMPERE, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.CURRENT}_{Units.ELECTRIC_CURRENT_AMPERE}", + device_class=SensorDeviceClass.CURRENT, + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for speed sensor + ( + BTHomeSensorDeviceClass.SPEED, + Units.SPEED_METERS_PER_SECOND, + ): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.SPEED}_{Units.SPEED_METERS_PER_SECOND}", + device_class=SensorDeviceClass.SPEED, + native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for UV index sensor + (BTHomeSensorDeviceClass.UV_INDEX, None,): SensorEntityDescription( + key=f"{BTHomeSensorDeviceClass.UV_INDEX}", + device_class=None, + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 4a0b9529ee7c25..355340d3ed3d25 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -36,6 +36,11 @@ "connectable": False, "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb", }, + { + "domain": "bthome", + "connectable": False, + "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb", + }, { "domain": "fjaraskupan", "connectable": False, diff --git a/requirements_all.txt b/requirements_all.txt index d8dab65484262c..7fea5716280127 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -482,7 +482,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==1.2.2 +bthome-ble==2.2.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 041b857733196f..a62c14f197d511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ brother==2.0.0 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==1.2.2 +bthome-ble==2.2.1 # homeassistant.components.buienradar buienradar==1.0.5 diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index 25ccb72edfad25..2951413b0e6d64 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -85,7 +85,7 @@ ) -def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBleak: +def make_bthome_v1_adv(address: str, payload: bytes) -> BluetoothServiceInfoBleak: """Make a dummy advertisement.""" return BluetoothServiceInfoBleak( name="Test Device", @@ -104,7 +104,7 @@ def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBlea ) -def make_encrypted_advertisement( +def make_encrypted_bthome_v1_adv( address: str, payload: bytes ) -> BluetoothServiceInfoBleak: """Make a dummy encrypted advertisement.""" @@ -123,3 +123,22 @@ def make_encrypted_advertisement( time=0, connectable=False, ) + + +def make_bthome_v2_adv(address: str, payload: bytes) -> BluetoothServiceInfoBleak: + """Make a dummy advertisement.""" + return BluetoothServiceInfoBleak( + name="Test Device", + address=address, + device=BLEDevice(address, None), + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fcd2-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000fcd2-0000-1000-8000-00805f9b34fb"], + source="local", + advertisement=generate_advertisement_data(local_name="Test Device"), + time=0, + connectable=False, + ) diff --git a/tests/components/bthome/test_binary_sensor.py b/tests/components/bthome/test_binary_sensor.py index 64b19b17a81441..99c3b310678814 100644 --- a/tests/components/bthome/test_binary_sensor.py +++ b/tests/components/bthome/test_binary_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.bthome.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON -from . import make_advertisement +from . import make_bthome_v1_adv, make_bthome_v2_adv from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info @@ -20,7 +20,7 @@ [ ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x10\x01", ), @@ -35,7 +35,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x11\x00", ), @@ -50,7 +50,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x0F\x01", ), @@ -65,14 +65,100 @@ ), ], ) -async def test_binary_sensors( +async def test_v1_binary_sensors( hass, mac_address, advertisement, bind_key, result, ): - """Test the different binary sensors.""" + """Test the different BTHome v1 binary sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac_address, + data={"bindkey": bind_key}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + advertisement, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == len(result) + for meas in result: + binary_sensor = hass.states.get(meas["binary_sensor_entity"]) + binary_sensor_attr = binary_sensor.attributes + assert binary_sensor.state == meas["expected_state"] + assert binary_sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "mac_address, advertisement, bind_key, result", + [ + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x10\x01", + ), + None, + [ + { + "binary_sensor_entity": "binary_sensor.test_device_18b2_power", + "friendly_name": "Test Device 18B2 Power", + "expected_state": STATE_ON, + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x11\x00", + ), + None, + [ + { + "binary_sensor_entity": "binary_sensor.test_device_18b2_opening", + "friendly_name": "Test Device 18B2 Opening", + "expected_state": STATE_OFF, + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0F\x01", + ), + None, + [ + { + "binary_sensor_entity": "binary_sensor.test_device_18b2_generic", + "friendly_name": "Test Device 18B2 Generic", + "expected_state": STATE_ON, + }, + ], + ), + ], +) +async def test_v2_binary_sensors( + hass, + mac_address, + advertisement, + bind_key, + result, +): + """Test the different BTHome v2 binary sensors.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=mac_address, diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 78b247aa393067..989fff1a25f3c5 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -1,24 +1,29 @@ """Test the BTHome sensors.""" +import logging + import pytest from homeassistant.components.bthome.const import DOMAIN from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT -from . import make_advertisement, make_encrypted_advertisement +from . import make_bthome_v1_adv, make_bthome_v2_adv, make_encrypted_bthome_v1_adv from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info +_LOGGER = logging.getLogger(__name__) + +# Tests for BTHome v1 @pytest.mark.parametrize( "mac_address, advertisement, bind_key, result", [ ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"#\x02\xca\t\x03\x03\xbf\x13", ), @@ -42,7 +47,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x00\xa8#\x02]\t\x03\x03\xb7\x18\x02\x01]", ), @@ -73,7 +78,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x00\x0c\x04\x04\x13\x8a\x01", ), @@ -90,7 +95,7 @@ ), ( "AA:BB:CC:DD:EE:FF", - make_advertisement( + make_bthome_v1_adv( "AA:BB:CC:DD:EE:FF", b"\x04\x05\x13\x8a\x14", ), @@ -107,7 +112,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x06\x5e\x1f", ), @@ -124,7 +129,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x07\x3e\x1d", ), @@ -141,7 +146,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x23\x08\xCA\x06", ), @@ -158,7 +163,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x02\x09\x60", ), @@ -174,7 +179,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x04\n\x13\x8a\x14", ), @@ -191,7 +196,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x04\x0b\x02\x1b\x00", ), @@ -208,7 +213,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x0c\x02\x0c", ), @@ -225,7 +230,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\r\x12\x0c\x03\x0e\x02\x1c", ), @@ -249,7 +254,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x12\xe2\x04", ), @@ -266,7 +271,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x133\x01", ), @@ -283,7 +288,7 @@ ), ( "A4:C1:38:8D:18:B2", - make_advertisement( + make_bthome_v1_adv( "A4:C1:38:8D:18:B2", b"\x03\x14\x02\x0c", ), @@ -300,7 +305,7 @@ ), ( "54:48:E6:8F:80:A5", - make_encrypted_advertisement( + make_encrypted_bthome_v1_adv( "54:48:E6:8F:80:A5", b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99', ), @@ -324,14 +329,577 @@ ), ], ) -async def test_sensors( +async def test_v1_sensors( + hass, + mac_address, + advertisement, + bind_key, + result, +): + """Test the different BTHome V1 sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac_address, + data={"bindkey": bind_key}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + inject_bluetooth_service_info( + hass, + advertisement, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == len(result) + + for meas in result: + sensor = hass.states.get(meas["sensor_entity"]) + sensor_attr = sensor.attributes + assert sensor.state == meas["expected_state"] + assert sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] + if ATTR_UNIT_OF_MEASUREMENT in sensor_attr: + # Some sensors don't have a unit of measurement + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] + assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +# Tests for BTHome V2 +@pytest.mark.parametrize( + "mac_address, advertisement, bind_key, result", + [ + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x02\xca\x09\x03\xbf\x13", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity", + "friendly_name": "Test Device 18B2 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "50.55", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x01\x5d\x02\x5d\x09\x03\xb7\x18", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "23.97", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity", + "friendly_name": "Test Device 18B2 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "63.27", + }, + { + "sensor_entity": "sensor.test_device_18b2_battery", + "friendly_name": "Test Device 18B2 Battery", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "93", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x04\x13\x8a\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_pressure", + "friendly_name": "Test Device 18B2 Pressure", + "unit_of_measurement": "mbar", + "state_class": "measurement", + "expected_state": "1008.83", + }, + ], + ), + ( + "AA:BB:CC:DD:EE:FF", + make_bthome_v2_adv( + "AA:BB:CC:DD:EE:FF", + b"\x40\x05\x13\x8a\x14", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_eeff_illuminance", + "friendly_name": "Test Device EEFF Illuminance", + "unit_of_measurement": "lx", + "state_class": "measurement", + "expected_state": "13460.67", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x06\x5E\x1F", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_mass", + "friendly_name": "Test Device 18B2 Mass", + "unit_of_measurement": "kg", + "state_class": "measurement", + "expected_state": "80.3", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x07\x3E\x1d", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_mass", + "friendly_name": "Test Device 18B2 Mass", + "unit_of_measurement": "lb", + "state_class": "measurement", + "expected_state": "74.86", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x08\xCA\x06", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_dew_point", + "friendly_name": "Test Device 18B2 Dew Point", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "17.38", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x09\x60", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_count", + "friendly_name": "Test Device 18B2 Count", + "state_class": "measurement", + "expected_state": "96", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0a\x13\x8a\x14", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_energy", + "friendly_name": "Test Device 18B2 Energy", + "unit_of_measurement": "kWh", + "state_class": "total_increasing", + "expected_state": "1346.067", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0b\x02\x1b\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_power", + "friendly_name": "Test Device 18B2 Power", + "unit_of_measurement": "W", + "state_class": "measurement", + "expected_state": "69.14", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0c\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_voltage", + "friendly_name": "Test Device 18B2 Voltage", + "unit_of_measurement": "V", + "state_class": "measurement", + "expected_state": "3.074", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x0d\x12\x0c\x0e\x02\x1c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_pm10", + "friendly_name": "Test Device 18B2 Pm10", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "7170", + }, + { + "sensor_entity": "sensor.test_device_18b2_pm25", + "friendly_name": "Test Device 18B2 Pm25", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "3090", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x12\xe2\x04", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_carbon_dioxide", + "friendly_name": "Test Device 18B2 Carbon Dioxide", + "unit_of_measurement": "ppm", + "state_class": "measurement", + "expected_state": "1250", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x133\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_volatile_organic_compounds", + "friendly_name": "Test Device 18B2 Volatile Organic Compounds", + "unit_of_measurement": "µg/m³", + "state_class": "measurement", + "expected_state": "307", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x14\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_moisture", + "friendly_name": "Test Device 18B2 Moisture", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "30.74", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x3F\x02\x0c", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_rotation", + "friendly_name": "Test Device 18B2 Rotation", + "unit_of_measurement": "°", + "state_class": "measurement", + "expected_state": "307.4", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x40\x0C\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_distance", + "friendly_name": "Test Device 18B2 Distance", + "unit_of_measurement": "mm", + "state_class": "measurement", + "expected_state": "12", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x41\x4E\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_distance", + "friendly_name": "Test Device 18B2 Distance", + "unit_of_measurement": "m", + "state_class": "measurement", + "expected_state": "7.8", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x42\x4E\x34\x00", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_duration", + "friendly_name": "Test Device 18B2 Duration", + "unit_of_measurement": "s", + "state_class": "measurement", + "expected_state": "13.39", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x43\x4E\x34", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_current", + "friendly_name": "Test Device 18B2 Current", + "unit_of_measurement": "A", + "state_class": "measurement", + "expected_state": "13.39", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x44\x4E\x34", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_speed", + "friendly_name": "Test Device 18B2 Speed", + "unit_of_measurement": "m/s", + "state_class": "measurement", + "expected_state": "133.9", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x45\x11\x01", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature", + "friendly_name": "Test Device 18B2 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "27.3", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x46\x32", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_uv_index", + "friendly_name": "Test Device 18B2 Uv Index", + "state_class": "measurement", + "expected_state": "5.0", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x02\xca\x09\x02\xcf\x09", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature_1", + "friendly_name": "Test Device 18B2 Temperature 1", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_18b2_temperature_2", + "friendly_name": "Test Device 18B2 Temperature 2", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.11", + }, + ], + ), + ( + "A4:C1:38:8D:18:B2", + make_bthome_v2_adv( + "A4:C1:38:8D:18:B2", + b"\x40\x02\xca\x09\x02\xcf\x09\x02\xcf\x08\x03\xb7\x18\x03\xb7\x17\x01\x5d", + ), + None, + [ + { + "sensor_entity": "sensor.test_device_18b2_temperature_1", + "friendly_name": "Test Device 18B2 Temperature 1", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_18b2_temperature_2", + "friendly_name": "Test Device 18B2 Temperature 2", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.11", + }, + { + "sensor_entity": "sensor.test_device_18b2_temperature_3", + "friendly_name": "Test Device 18B2 Temperature 3", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "22.55", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity_1", + "friendly_name": "Test Device 18B2 Humidity 1", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "63.27", + }, + { + "sensor_entity": "sensor.test_device_18b2_humidity_2", + "friendly_name": "Test Device 18B2 Humidity 2", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "60.71", + }, + { + "sensor_entity": "sensor.test_device_18b2_battery", + "friendly_name": "Test Device 18B2 Battery", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "93", + }, + ], + ), + ( + "54:48:E6:8F:80:A5", + make_bthome_v2_adv( + "54:48:E6:8F:80:A5", + b"\x41\xa4\x72\x66\xc9\x5f\x73\x00\x11\x22\x33\xb7\xce\xd8\xe5", + ), + "231d39c1d7cc1ab1aee224cd096db932", + [ + { + "sensor_entity": "sensor.test_device_80a5_temperature", + "friendly_name": "Test Device 80A5 Temperature", + "unit_of_measurement": "°C", + "state_class": "measurement", + "expected_state": "25.06", + }, + { + "sensor_entity": "sensor.test_device_80a5_humidity", + "friendly_name": "Test Device 80A5 Humidity", + "unit_of_measurement": "%", + "state_class": "measurement", + "expected_state": "50.55", + }, + ], + ), + ], +) +async def test_v2_sensors( hass, mac_address, advertisement, bind_key, result, ): - """Test the different measurement sensors.""" + """Test the different BTHome V2 sensors.""" entry = MockConfigEntry( domain=DOMAIN, unique_id=mac_address, @@ -352,12 +920,14 @@ async def test_sensors( assert len(hass.states.async_all()) == len(result) for meas in result: + _LOGGER.error(meas) sensor = hass.states.get(meas["sensor_entity"]) + _LOGGER.error(hass.states) sensor_attr = sensor.attributes assert sensor.state == meas["expected_state"] assert sensor_attr[ATTR_FRIENDLY_NAME] == meas["friendly_name"] if ATTR_UNIT_OF_MEASUREMENT in sensor_attr: - # Count sensor does not have a unit of measurement + # Some sensors don't have a unit of measurement assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == meas["unit_of_measurement"] assert sensor_attr[ATTR_STATE_CLASS] == meas["state_class"] assert await hass.config_entries.async_unload(entry.entry_id) From ec316e94ed6519e24b3274d4626b4bd8b431be2f Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 9 Nov 2022 16:35:30 +0200 Subject: [PATCH 342/394] RuuviTag BLE sensor support (#81327) --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/ruuvitag_ble/__init__.py | 49 +++++ .../components/ruuvitag_ble/config_flow.py | 94 ++++++++ .../components/ruuvitag_ble/const.py | 3 + .../components/ruuvitag_ble/manifest.json | 18 ++ .../components/ruuvitag_ble/sensor.py | 165 ++++++++++++++ homeassistant/generated/bluetooth.py | 8 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ruuvitag_ble/__init__.py | 1 + tests/components/ruuvitag_ble/fixtures.py | 26 +++ .../ruuvitag_ble/test_config_flow.py | 202 ++++++++++++++++++ tests/components/ruuvitag_ble/test_sensor.py | 46 ++++ 17 files changed, 638 insertions(+) create mode 100644 homeassistant/components/ruuvitag_ble/__init__.py create mode 100644 homeassistant/components/ruuvitag_ble/config_flow.py create mode 100644 homeassistant/components/ruuvitag_ble/const.py create mode 100644 homeassistant/components/ruuvitag_ble/manifest.json create mode 100644 homeassistant/components/ruuvitag_ble/sensor.py create mode 100644 tests/components/ruuvitag_ble/__init__.py create mode 100644 tests/components/ruuvitag_ble/fixtures.py create mode 100644 tests/components/ruuvitag_ble/test_config_flow.py create mode 100644 tests/components/ruuvitag_ble/test_sensor.py diff --git a/.strict-typing b/.strict-typing index 330424faec7594..67b5d44b97b048 100644 --- a/.strict-typing +++ b/.strict-typing @@ -229,6 +229,7 @@ homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.rpi_power.* homeassistant.components.rtsp_to_webrtc.* +homeassistant.components.ruuvitag_ble.* homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.schedule.* diff --git a/CODEOWNERS b/CODEOWNERS index db01573e8085a2..1f45098e4f81a0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -964,6 +964,8 @@ build.json @home-assistant/supervisor /tests/components/rtsp_to_webrtc/ @allenporter /homeassistant/components/ruckus_unleashed/ @gabe565 /tests/components/ruckus_unleashed/ @gabe565 +/homeassistant/components/ruuvitag_ble/ @akx +/tests/components/ruuvitag_ble/ @akx /homeassistant/components/sabnzbd/ @shaiu /tests/components/sabnzbd/ @shaiu /homeassistant/components/safe_mode/ @home-assistant/core diff --git a/homeassistant/components/ruuvitag_ble/__init__.py b/homeassistant/components/ruuvitag_ble/__init__.py new file mode 100644 index 00000000000000..5e30820f837173 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/__init__.py @@ -0,0 +1,49 @@ +"""The ruuvitag_ble integration.""" +from __future__ import annotations + +import logging + +from ruuvitag_ble import RuuvitagBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ruuvitag BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = RuuvitagBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + 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): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ruuvitag_ble/config_flow.py b/homeassistant/components/ruuvitag_ble/config_flow.py new file mode 100644 index 00000000000000..620b901f4fe346 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for ruuvitag_ble.""" +from __future__ import annotations + +from typing import Any + +from ruuvitag_ble import RuuvitagBluetoothDeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class RuuvitagConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for ruuvitag_ble.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: RuuvitagBluetoothDeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = RuuvitagBluetoothDeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = RuuvitagBluetoothDeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/ruuvitag_ble/const.py b/homeassistant/components/ruuvitag_ble/const.py new file mode 100644 index 00000000000000..0df74a24eea439 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the ruuvitag_ble integration.""" + +DOMAIN = "ruuvitag_ble" diff --git a/homeassistant/components/ruuvitag_ble/manifest.json b/homeassistant/components/ruuvitag_ble/manifest.json new file mode 100644 index 00000000000000..a3500fca7c6e43 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/manifest.json @@ -0,0 +1,18 @@ +{ + "domain": "ruuvitag_ble", + "name": "RuuviTag BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ruuvitag_ble", + "bluetooth": [ + { + "manufacturer_id": 1177 + }, + { + "local_name": "Ruuvi *" + } + ], + "requirements": ["ruuvitag-ble==0.1.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@akx"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/ruuvitag_ble/sensor.py b/homeassistant/components/ruuvitag_ble/sensor.py new file mode 100644 index 00000000000000..463d6da2de2718 --- /dev/null +++ b/homeassistant/components/ruuvitag_ble/sensor.py @@ -0,0 +1,165 @@ +"""Support for RuuviTag sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from sensor_state_data import ( + DeviceKey, + SensorDescription, + SensorDeviceClass, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries, const +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (SensorDeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{SensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=const.TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (SensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{SensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=const.PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (SensorDeviceClass.PRESSURE, Units.PRESSURE_HPA): SensorEntityDescription( + key=f"{SensorDeviceClass.PRESSURE}_{Units.PRESSURE_HPA}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=const.PRESSURE_HPA, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + SensorDeviceClass.VOLTAGE, + Units.ELECTRIC_POTENTIAL_MILLIVOLT, + ): SensorEntityDescription( + key=f"{SensorDeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_MILLIVOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=const.ELECTRIC_POTENTIAL_MILLIVOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + SensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{SensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=const.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + (SensorDeviceClass.COUNT, None): SensorEntityDescription( + key="movement_counter", + device_class=SensorDeviceClass.COUNT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo() + if sensor_device_info.name is not None: + hass_device_info[const.ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[const.ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[const.ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def _to_sensor_key( + description: SensorDescription, +) -> tuple[SensorDeviceClass, Units | None]: + assert description.device_class is not None + return (description.device_class, description.native_unit_of_measurement) + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + _to_sensor_key(description) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if _to_sensor_key(description) in SENSOR_DESCRIPTIONS + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Ruuvitag BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + RuuvitagBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class RuuvitagBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Ruuvitag BLE sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 355340d3ed3d25..6fa2342b5a4a42 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -263,6 +263,14 @@ "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb", "connectable": False, }, + { + "domain": "ruuvitag_ble", + "manufacturer_id": 1177, + }, + { + "domain": "ruuvitag_ble", + "local_name": "Ruuvi *", + }, { "domain": "sensorpro", "manufacturer_id": 43605, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 05e352ab2b7840..46aefeebb222e5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -334,6 +334,7 @@ "rpi_power", "rtsp_to_webrtc", "ruckus_unleashed", + "ruuvitag_ble", "sabnzbd", "samsungtv", "screenlogic", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index bbb7928f68f097..ed1aa0a3647e87 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4475,6 +4475,12 @@ } } }, + "ruuvitag_ble": { + "name": "RuuviTag BLE", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push" + }, "sabnzbd": { "name": "SABnzbd", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index 988701393d6429..b30d64163bc2e8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2043,6 +2043,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ruuvitag_ble.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.samsungtv.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 7fea5716280127..c9106d2ce39627 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2217,6 +2217,9 @@ russound==0.1.9 # homeassistant.components.russound_rio russound_rio==0.1.8 +# homeassistant.components.ruuvitag_ble +ruuvitag-ble==0.1.1 + # homeassistant.components.yamaha rxv==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a62c14f197d511..65966609ecc115 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1532,6 +1532,9 @@ rpi-bad-power==0.1.0 # homeassistant.components.rtsp_to_webrtc rtsp-to-webrtc==0.5.1 +# homeassistant.components.ruuvitag_ble +ruuvitag-ble==0.1.1 + # homeassistant.components.yamaha rxv==0.7.0 diff --git a/tests/components/ruuvitag_ble/__init__.py b/tests/components/ruuvitag_ble/__init__.py new file mode 100644 index 00000000000000..e39900689ccb82 --- /dev/null +++ b/tests/components/ruuvitag_ble/__init__.py @@ -0,0 +1 @@ +"""Test package for RuuviTag BLE sensor integration.""" diff --git a/tests/components/ruuvitag_ble/fixtures.py b/tests/components/ruuvitag_ble/fixtures.py new file mode 100644 index 00000000000000..26eee1bac5e0cd --- /dev/null +++ b/tests/components/ruuvitag_ble/fixtures.py @@ -0,0 +1,26 @@ +"""Fixtures for testing RuuviTag BLE.""" +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +RUUVITAG_SERVICE_INFO = BluetoothServiceInfo( + name="RuuviTag 0911", + address="01:03:05:07:09:11", # Ignored (the payload encodes the correct MAC) + rssi=-60, + manufacturer_data={ + 1177: b"\x05\x05\xa0`\xa0\xc8\x9a\xfd4\x02\x8c\xff\x00cvriv\xde\xad{?\xef\xaf" + }, + service_data={}, + service_uuids=[], + source="local", +) +CONFIGURED_NAME = "RuuviTag EFAF" +CONFIGURED_PREFIX = "ruuvitag_efaf" diff --git a/tests/components/ruuvitag_ble/test_config_flow.py b/tests/components/ruuvitag_ble/test_config_flow.py new file mode 100644 index 00000000000000..1482f9b61b0bb0 --- /dev/null +++ b/tests/components/ruuvitag_ble/test_config_flow.py @@ -0,0 +1,202 @@ +"""Test the Ruuvitag config flow.""" + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.ruuvitag_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from .fixtures import CONFIGURED_NAME, NOT_RUUVITAG_SERVICE_INFO, RUUVITAG_SERVICE_INFO + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Mock bluetooth for all tests in this module.""" + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address + + +async def test_async_step_bluetooth_not_ruuvitag(hass): + """Test discovery via bluetooth not ruuvitag.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": RUUVITAG_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=RUUVITAG_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": RUUVITAG_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=RUUVITAG_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=RUUVITAG_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=RUUVITAG_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.ruuvitag_ble.config_flow.async_discovered_service_info", + return_value=[RUUVITAG_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.ruuvitag_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": RUUVITAG_SERVICE_INFO.address}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == CONFIGURED_NAME + assert result2["data"] == {} + assert result2["result"].unique_id == RUUVITAG_SERVICE_INFO.address diff --git a/tests/components/ruuvitag_ble/test_sensor.py b/tests/components/ruuvitag_ble/test_sensor.py new file mode 100644 index 00000000000000..2f9c027293ae70 --- /dev/null +++ b/tests/components/ruuvitag_ble/test_sensor.py @@ -0,0 +1,46 @@ +"""Test the Ruuvitag BLE sensors.""" + +from __future__ import annotations + +from homeassistant.components.ruuvitag_ble.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant + +from .fixtures import CONFIGURED_NAME, CONFIGURED_PREFIX, RUUVITAG_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors(enable_bluetooth, hass: HomeAssistant): + """Test the RuuviTag BLE sensors.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=RUUVITAG_SERVICE_INFO.address) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info( + hass, + RUUVITAG_SERVICE_INFO, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) >= 4 + + for sensor, value, unit, state_class in [ + ("temperature", "7.2", "°C", "measurement"), + ("humidity", "61.84", "%", "measurement"), + ("pressure", "1013.54", "hPa", "measurement"), + ("voltage", "2395", "mV", "measurement"), + ]: + state = hass.states.get(f"sensor.{CONFIGURED_PREFIX}_{sensor}") + assert state is not None + assert state.state == value + name_lower = state.attributes[ATTR_FRIENDLY_NAME].lower() + assert name_lower == f"{CONFIGURED_NAME} {sensor}".lower() + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == unit + assert state.attributes[ATTR_STATE_CLASS] == state_class + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 5a6f7e66cb6fcdf324820c14dab34d7bdbf57946 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 9 Nov 2022 16:36:03 +0200 Subject: [PATCH 343/394] Refactor + strictly-type image component (#81808) * image: refactor size validation to use partition * image: give _generate_thumbnail types and use partition * image: become strictly typed --- .strict-typing | 1 + homeassistant/components/image/__init__.py | 72 ++++++++++++++++------ mypy.ini | 10 +++ 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/.strict-typing b/.strict-typing index 67b5d44b97b048..d45fe269638326 100644 --- a/.strict-typing +++ b/.strict-typing @@ -151,6 +151,7 @@ homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* homeassistant.components.ibeacon.* +homeassistant.components.image.* homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* diff --git a/homeassistant/components/image/__init__.py b/homeassistant/components/image/__init__.py index 51263e38ab7d82..23ab393aabd9da 100644 --- a/homeassistant/components/image/__init__.py +++ b/homeassistant/components/image/__init__.py @@ -6,6 +6,7 @@ import pathlib import secrets import shutil +from typing import Any from PIL import Image, ImageOps, UnidentifiedImageError from aiohttp import hdrs, web @@ -71,7 +72,7 @@ def __init__(self, hass: HomeAssistant, image_dir: pathlib.Path) -> None: self.async_add_listener(self._change_listener) self.image_dir = image_dir - async def _process_create_data(self, data: dict) -> dict: + async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]: """Validate the config is valid.""" data = self.CREATE_SCHEMA(dict(data)) uploaded_file: FileField = data["file"] @@ -88,7 +89,7 @@ async def _process_create_data(self, data: dict) -> dict: return data - def _move_data(self, data): + def _move_data(self, data: dict[str, Any]) -> int: """Move data.""" uploaded_file: FileField = data.pop("file") @@ -119,15 +120,24 @@ def _move_data(self, data): return media_file.stat().st_size @callback - def _get_suggested_id(self, info: dict) -> str: + def _get_suggested_id(self, info: dict[str, Any]) -> str: """Suggest an ID based on the config.""" - return info[CONF_ID] + return str(info[CONF_ID]) - async def _update_data(self, data: dict, update_data: dict) -> dict: + async def _update_data( + self, + data: dict[str, Any], + update_data: dict[str, Any], + ) -> dict[str, Any]: """Return a new updated data object.""" return {**data, **self.UPDATE_SCHEMA(update_data)} - async def _change_listener(self, change_type, item_id, data): + async def _change_listener( + self, + change_type: str, + item_id: str, + data: dict[str, Any], + ) -> None: """Handle change.""" if change_type != collection.CHANGE_REMOVED: return @@ -141,7 +151,7 @@ class ImageUploadView(HomeAssistantView): url = "/api/image/upload" name = "api:image:upload" - async def post(self, request): + async def post(self, request: web.Request) -> web.Response: """Handle upload.""" # Increase max payload request._client_max_size = MAX_SIZE # pylint: disable=protected-access @@ -159,26 +169,27 @@ class ImageServeView(HomeAssistantView): requires_auth = False def __init__( - self, image_folder: pathlib.Path, image_collection: ImageStorageCollection + self, + image_folder: pathlib.Path, + image_collection: ImageStorageCollection, ) -> None: """Initialize image serve view.""" self.transform_lock = asyncio.Lock() self.image_folder = image_folder self.image_collection = image_collection - async def get(self, request: web.Request, image_id: str, filename: str): + async def get( + self, + request: web.Request, + image_id: str, + filename: str, + ) -> web.FileResponse: """Serve image.""" - image_size = filename.split("-", 1)[0] try: - parts = image_size.split("x", 1) - width = int(parts[0]) - height = int(parts[1]) + width, height = _validate_size_from_filename(filename) except (ValueError, IndexError) as err: raise web.HTTPBadRequest from err - if not width or width != height or width not in VALID_SIZES: - raise web.HTTPBadRequest - image_info = self.image_collection.data.get(image_id) if image_info is None: @@ -205,8 +216,33 @@ async def get(self, request: web.Request, image_id: str, filename: str): ) -def _generate_thumbnail(original_path, content_type, target_path, target_size): +def _generate_thumbnail( + original_path: pathlib.Path, + content_type: str, + target_path: pathlib.Path, + target_size: tuple[int, int], +) -> None: """Generate a size.""" image = ImageOps.exif_transpose(Image.open(original_path)) image.thumbnail(target_size) - image.save(target_path, format=content_type.split("/", 1)[1]) + image.save(target_path, format=content_type.partition("/")[-1]) + + +def _validate_size_from_filename(filename: str) -> tuple[int, int]: + """Parse image size from the given filename (of the form WIDTHxHEIGHT-filename). + + >>> _validate_size_from_filename("100x100-image.png") + (100, 100) + >>> _validate_size_from_filename("jeff.png") + Traceback (most recent call last): + ... + """ + image_size = filename.partition("-")[0] + if not image_size: + raise ValueError("Invalid filename") + width_s, _, height_s = image_size.partition("x") + width = int(width_s) + height = int(height_s) + if not width or width != height or width not in VALID_SIZES: + raise ValueError(f"Invalid size {image_size}") + return (width, height) diff --git a/mypy.ini b/mypy.ini index b30d64163bc2e8..e30a78dab8c0e8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1263,6 +1263,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.image.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.image_processing.*] check_untyped_defs = true disallow_incomplete_defs = true From 8874bf77917749a38fe709124515a55673bf3ba5 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 9 Nov 2022 08:44:30 -0600 Subject: [PATCH 344/394] Change life360 timeouts & retries (#81799) Change from single timeout of 10 to socket timeout of 15, total timeout of 60, and retry up to 3 times. Bump life360 package to 5.3.0. --- homeassistant/components/life360/const.py | 5 ++++- homeassistant/components/life360/coordinator.py | 2 ++ homeassistant/components/life360/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py index d148a06c63433d..333ce14fbf6fa1 100644 --- a/homeassistant/components/life360/const.py +++ b/homeassistant/components/life360/const.py @@ -3,11 +3,14 @@ from datetime import timedelta import logging +from aiohttp import ClientTimeout + DOMAIN = "life360" LOGGER = logging.getLogger(__package__) ATTRIBUTION = "Data provided by life360.com" -COMM_TIMEOUT = 10 +COMM_MAX_RETRIES = 3 +COMM_TIMEOUT = ClientTimeout(sock_connect=15, total=60) SPEED_FACTOR_MPH = 2.25 SPEED_DIGITS = 1 UPDATE_INTERVAL = timedelta(seconds=10) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index 0b9641bfcae6c1..b7121cc7fdb995 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -26,6 +26,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM from .const import ( + COMM_MAX_RETRIES, COMM_TIMEOUT, CONF_AUTHORIZATION, DOMAIN, @@ -106,6 +107,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: self._api = Life360( session=async_get_clientsession(hass), timeout=COMM_TIMEOUT, + max_retries=COMM_MAX_RETRIES, authorization=entry.data[CONF_AUTHORIZATION], ) self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 8f0c44f342b437..eb3290e41e11af 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "codeowners": ["@pnbruckner"], - "requirements": ["life360==5.1.1"], + "requirements": ["life360==5.3.0"], "iot_class": "cloud_polling", "loggers": ["life360"] } diff --git a/requirements_all.txt b/requirements_all.txt index c9106d2ce39627..bfd44464578faa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1019,7 +1019,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.1 +life360==5.3.0 # homeassistant.components.osramlightify lightify==1.0.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65966609ecc115..96c757c9feccc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -757,7 +757,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.life360 -life360==5.1.1 +life360==5.3.0 # homeassistant.components.logi_circle logi_circle==0.2.3 From e690db9ba6b07bb36bc1f6c90db18fa22fb3a79c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 9 Nov 2022 16:51:33 +0200 Subject: [PATCH 345/394] Upgrade huawei-lte-api to 1.6.7, fixes empty username issues (#81751) Recentish versions of huawei-lte-api behave differently with regards to empty/default username compared to the older versions this integration was originally written against. 1.6.5+ changes the behavior so that our existing implementation works as expected when no username is supplied for the config entry. https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.4 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.5 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.6 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6.7 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index c658fff1b0f2a4..2c777aa433937f 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.3", + "huawei-lte-api==1.6.7", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index bfd44464578faa..d0951302aff887 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -902,7 +902,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.3 +huawei-lte-api==1.6.7 # homeassistant.components.hydrawise hydrawiser==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96c757c9feccc7..b34ef5074d4227 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -679,7 +679,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.3 +huawei-lte-api==1.6.7 # homeassistant.components.hyperion hyperion-py==0.7.5 From 9de4d7cba3177f9abe5634d96ef3edc4048841d6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 9 Nov 2022 15:27:36 +0000 Subject: [PATCH 346/394] Fix homekit_controller climate entity not becoming active when changing modes (#81868) --- .../components/homekit_controller/climate.py | 1 + .../homekit_controller/test_climate.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index de42243a6bbb78..41e887251213f2 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -209,6 +209,7 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: ) await self.async_put_characteristics( { + CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE, CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT[ hvac_mode ], diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 0f10f0f9fa0f4d..bf544c5aff4745 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -760,6 +760,42 @@ async def test_heater_cooler_change_thermostat_state(hass, utcnow): ) +async def test_can_turn_on_after_off(hass, utcnow): + """ + Test that we always force device from inactive to active when setting mode. + + This is a regression test for #81863. + """ + helper = await setup_test_component(hass, create_heater_cooler_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVACMode.OFF}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.INACTIVE, + }, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVACMode.HEAT}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.HEATER_COOLER, + { + CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE, + CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TargetHeaterCoolerStateValues.HEAT, + }, + ) + + async def test_heater_cooler_change_thermostat_temperature(hass, utcnow): """Test that we can change the target temperature.""" helper = await setup_test_component(hass, create_heater_cooler_service) From 84725f15a61f45161706414e0c8dd70856956d27 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 9 Nov 2022 23:28:28 +0800 Subject: [PATCH 347/394] Use IntEnum for stream orientation (#81835) * Use IntEnum for stream orientation * Rename enum values * Add comments * Fix import --- homeassistant/components/camera/__init__.py | 3 ++- homeassistant/components/camera/prefs.py | 19 +++++++------ homeassistant/components/stream/__init__.py | 8 +++--- homeassistant/components/stream/core.py | 18 +++++++++++-- homeassistant/components/stream/fmp4utils.py | 28 +++++++++++--------- tests/components/camera/test_init.py | 4 +-- 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 136cd3b05f1a7b..da88dc49a5b1cb 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -30,6 +30,7 @@ from homeassistant.components.stream import ( FORMAT_CONTENT_TYPE, OUTPUT_FORMATS, + Orientation, Stream, create_stream, ) @@ -869,7 +870,7 @@ async def websocket_get_prefs( vol.Required("type"): "camera/update_prefs", vol.Required("entity_id"): cv.entity_id, vol.Optional(PREF_PRELOAD_STREAM): bool, - vol.Optional(PREF_ORIENTATION): vol.All(int, vol.Range(min=1, max=8)), + vol.Optional(PREF_ORIENTATION): vol.Coerce(Orientation), } ) @websocket_api.async_response diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 1107da2ba385ff..fac93df474ee40 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -3,6 +3,7 @@ from typing import Final, Union, cast +from homeassistant.components.stream import Orientation from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er @@ -18,11 +19,11 @@ class CameraEntityPreferences: """Handle preferences for camera entity.""" - def __init__(self, prefs: dict[str, bool | int]) -> None: + def __init__(self, prefs: dict[str, bool | Orientation]) -> None: """Initialize prefs.""" self._prefs = prefs - def as_dict(self) -> dict[str, bool | int]: + def as_dict(self) -> dict[str, bool | Orientation]: """Return dictionary version.""" return self._prefs @@ -32,9 +33,11 @@ def preload_stream(self) -> bool: return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False)) @property - def orientation(self) -> int: + def orientation(self) -> Orientation: """Return the current stream orientation settings.""" - return self._prefs.get(PREF_ORIENTATION, 1) + return cast( + Orientation, self._prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM) + ) class CameraPreferences: @@ -45,11 +48,11 @@ def __init__(self, hass: HomeAssistant) -> None: self._hass = hass # The orientation prefs are stored in in the entity registry options # The preload_stream prefs are stored in this Store - self._store = Store[dict[str, dict[str, Union[bool, int]]]]( + self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]]( hass, STORAGE_VERSION, STORAGE_KEY ) # Local copy of the preload_stream prefs - self._prefs: dict[str, dict[str, bool | int]] | None = None + self._prefs: dict[str, dict[str, bool | Orientation]] | None = None async def async_initialize(self) -> None: """Finish initializing the preferences.""" @@ -63,9 +66,9 @@ async def async_update( entity_id: str, *, preload_stream: bool | UndefinedType = UNDEFINED, - orientation: int | UndefinedType = UNDEFINED, + orientation: Orientation | UndefinedType = UNDEFINED, stream_options: dict[str, str] | UndefinedType = UNDEFINED, - ) -> dict[str, bool | int]: + ) -> dict[str, bool | Orientation]: """Update camera preferences. Returns a dict with the preferences on success. diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 559de0940909ca..01cd3c2962cb0c 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -63,6 +63,7 @@ STREAM_SETTINGS_NON_LL_HLS, IdleTimer, KeyFrameConverter, + Orientation, StreamOutput, StreamSettings, ) @@ -82,6 +83,7 @@ "SOURCE_TIMEOUT", "Stream", "create_stream", + "Orientation", ] _LOGGER = logging.getLogger(__name__) @@ -229,7 +231,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: part_target_duration=conf[CONF_PART_DURATION], hls_advance_part_limit=max(int(3 / conf[CONF_PART_DURATION]), 3), hls_part_timeout=2 * conf[CONF_PART_DURATION], - orientation=1, + orientation=Orientation.NO_TRANSFORM, ) else: hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS @@ -292,12 +294,12 @@ def __init__( self._diagnostics = Diagnostics() @property - def orientation(self) -> int: + def orientation(self) -> Orientation: """Return the current orientation setting.""" return self._stream_settings.orientation @orientation.setter - def orientation(self, value: int) -> None: + def orientation(self, value: Orientation) -> None: """Set the stream orientation setting.""" self._stream_settings.orientation = value diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 0fa57913269d36..31654f7d0dbbde 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -5,6 +5,7 @@ from collections import deque from collections.abc import Callable, Coroutine, Iterable import datetime +from enum import IntEnum import logging from typing import TYPE_CHECKING, Any @@ -35,6 +36,19 @@ PROVIDERS: Registry[str, type[StreamOutput]] = Registry() +class Orientation(IntEnum): + """Orientations for stream transforms. These are based on EXIF orientation tags.""" + + NO_TRANSFORM = 1 + MIRROR = 2 + ROTATE_180 = 3 + FLIP = 4 + ROTATE_LEFT_AND_FLIP = 5 + ROTATE_LEFT = 6 + ROTATE_RIGHT_AND_FLIP = 7 + ROTATE_RIGHT = 8 + + @attr.s(slots=True) class StreamSettings: """Stream settings.""" @@ -44,7 +58,7 @@ class StreamSettings: part_target_duration: float = attr.ib() hls_advance_part_limit: int = attr.ib() hls_part_timeout: float = attr.ib() - orientation: int = attr.ib() + orientation: Orientation = attr.ib() STREAM_SETTINGS_NON_LL_HLS = StreamSettings( @@ -53,7 +67,7 @@ class StreamSettings: part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, hls_advance_part_limit=3, hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - orientation=1, + orientation=Orientation.NO_TRANSFORM, ) diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index 35d32d5b0e395f..5ec27a1768c639 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -6,6 +6,8 @@ from homeassistant.exceptions import HomeAssistantError +from .core import Orientation + if TYPE_CHECKING: from io import BufferedIOBase @@ -179,22 +181,24 @@ def read_init(bytes_io: BufferedIOBase) -> bytes: ROTATE_RIGHT_FLIP = (ZERO32 + ONE32 + ZERO32) + (ONE32 + ZERO32 + ZERO32) TRANSFORM_MATRIX_TOP = ( - # The first two entries are just to align the indices with the EXIF orientation tags - b"", - b"", - MIRROR, - ROTATE_180, - FLIP, - ROTATE_LEFT_FLIP, - ROTATE_LEFT, - ROTATE_RIGHT_FLIP, - ROTATE_RIGHT, + # The index into this tuple corresponds to the EXIF orientation tag + # Only index values of 2 through 8 are used + # The first two entries are just to keep everything aligned + b"", # 0 + b"", # 1 + MIRROR, # 2 + ROTATE_180, # 3 + FLIP, # 4 + ROTATE_LEFT_FLIP, # 5 + ROTATE_LEFT, # 6 + ROTATE_RIGHT_FLIP, # 7 + ROTATE_RIGHT, # 8 ) -def transform_init(init: bytes, orientation: int) -> bytes: +def transform_init(init: bytes, orientation: Orientation) -> bytes: """Change the transformation matrix in the header.""" - if orientation == 1: + if orientation == Orientation.NO_TRANSFORM: return init # Find moov moov_location = next(find_box(init, b"moov")) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 71415284d35ac7..9bf35ec55fa33f 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -367,7 +367,7 @@ async def test_websocket_update_orientation_prefs(hass, hass_ws_client, mock_cam assert response["success"] er_camera_prefs = registry.async_get("camera.demo_uniquecamera").options[DOMAIN] - assert er_camera_prefs[PREF_ORIENTATION] == 3 + assert er_camera_prefs[PREF_ORIENTATION] == camera.Orientation.ROTATE_180 assert response["result"][PREF_ORIENTATION] == er_camera_prefs[PREF_ORIENTATION] # Check that the preference was saved await client.send_json( @@ -375,7 +375,7 @@ async def test_websocket_update_orientation_prefs(hass, hass_ws_client, mock_cam ) msg = await client.receive_json() # orientation entry for this camera should have been added - assert msg["result"]["orientation"] == 3 + assert msg["result"]["orientation"] == camera.Orientation.ROTATE_180 async def test_play_stream_service_no_source(hass, mock_camera, mock_stream): From f9ff23a2c86711b60c03dad8eb88ac9e16296e34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 09:36:46 -0600 Subject: [PATCH 348/394] Fix benign typo in test_config_entries.py (#81789) * Fix typo in test_config_entries.py * touch ups * Update tests/test_config_entries.py Co-authored-by: Martin Hjelmare Co-authored-by: Erik Montnemery Co-authored-by: Martin Hjelmare --- tests/test_config_entries.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 080b3cdf5f1a04..96d032f771e6c1 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3316,17 +3316,17 @@ async def test_reauth(hass): assert entry.entry_id != entry2.entry_id - # Check we can't start duplicate flows + # Check that we can't start duplicate reauth flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 1 - # Check we can't start duplicate when the context context is different + # Check that we can't start duplicate reauth flows when the context is different entry.async_start_reauth(hass, {"diff": "diff"}) await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 1 - # Check we can start a reauth for a different entry + # Check that we can start a reauth flow for a different entry entry2.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress()) == 2 From f3e85b649254733e20aace24a9b6b648651ae1c2 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Wed, 9 Nov 2022 17:58:20 +0200 Subject: [PATCH 349/394] Deduplicate blackening (#81802) --- .../generated/application_credentials.py | 2 +- homeassistant/generated/bluetooth.py | 119 +-- homeassistant/generated/config_flows.py | 22 +- homeassistant/generated/currencies.py | 2 +- homeassistant/generated/dhcp.py | 979 ++++++++++++++---- homeassistant/generated/mqtt.py | 2 +- homeassistant/generated/ssdp.py | 2 +- homeassistant/generated/usb.py | 66 +- homeassistant/generated/zeroconf.py | 126 +-- script/currencies.py | 65 +- script/hassfest/application_credentials.py | 15 +- script/hassfest/bluetooth.py | 19 +- script/hassfest/config_flow.py | 15 +- script/hassfest/dhcp.py | 18 +- script/hassfest/mqtt.py | 15 +- script/hassfest/serializer.py | 103 +- script/hassfest/ssdp.py | 22 +- script/hassfest/usb.py | 15 +- script/hassfest/zeroconf.py | 25 +- 19 files changed, 1105 insertions(+), 527 deletions(-) diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index d2e16f2b914234..31e73418c5efd8 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -1,4 +1,4 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 6fa2342b5a4a42..7fa3f363093996 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -1,7 +1,8 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ + from __future__ import annotations BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ @@ -10,41 +11,40 @@ "manufacturer_id": 820, }, { + "connectable": False, "domain": "aranet", "manufacturer_id": 1794, "service_uuid": "f0cd1400-95da-4f4b-9ac8-aa55d312af0c", - "connectable": False, }, { + "connectable": False, "domain": "aranet", "manufacturer_id": 1794, "service_uuid": "0000fce0-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "bluemaestro", "manufacturer_id": 307, - "connectable": False, }, { - "domain": "bthome", "connectable": False, + "domain": "bthome", "service_data_uuid": "0000181c-0000-1000-8000-00805f9b34fb", }, { - "domain": "bthome", "connectable": False, + "domain": "bthome", "service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb", }, { - "domain": "bthome", "connectable": False, + "domain": "bthome", "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb", }, { - "domain": "fjaraskupan", "connectable": False, - "manufacturer_id": 20296, + "domain": "fjaraskupan", "manufacturer_data_start": [ 79, 68, @@ -53,143 +53,144 @@ 65, 82, ], + "manufacturer_id": 20296, }, { + "connectable": False, "domain": "govee_ble", "local_name": "Govee*", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "local_name": "GVH5*", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "local_name": "B5178*", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 6966, "service_uuid": "00008451-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 63391, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 26589, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 57391, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 53579, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 43682, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 59970, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 63585, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 10032, "service_uuid": "00008251-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "govee_ble", "manufacturer_id": 19506, "service_uuid": "00001801-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { "domain": "homekit_controller", - "manufacturer_id": 76, "manufacturer_data_start": [ 6, ], + "manufacturer_id": 76, }, { "domain": "ibeacon", - "manufacturer_id": 76, "manufacturer_data_start": [ 2, 21, ], + "manufacturer_id": 76, }, { + "connectable": False, "domain": "inkbird", "local_name": "sps", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "Inkbird*", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "iBBQ*", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "xBBQ*", - "connectable": False, }, { + "connectable": False, "domain": "inkbird", "local_name": "tps", - "connectable": False, }, { - "domain": "kegtron", "connectable": False, + "domain": "kegtron", "manufacturer_id": 65535, }, { @@ -240,28 +241,28 @@ "manufacturer_id": 13, }, { + "connectable": False, "domain": "moat", "local_name": "Moat_S*", - "connectable": False, }, { "domain": "oralb", "manufacturer_id": 220, }, { + "connectable": False, "domain": "qingping", "local_name": "Qingping*", - "connectable": False, }, { + "connectable": False, "domain": "qingping", "local_name": "Lee Guitars*", - "connectable": False, }, { + "connectable": False, "domain": "qingping", "service_data_uuid": "0000fdcd-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { "domain": "ruuvitag_ble", @@ -272,31 +273,31 @@ "local_name": "Ruuvi *", }, { + "connectable": False, "domain": "sensorpro", - "manufacturer_id": 43605, "manufacturer_data_start": [ 1, 1, 164, 193, ], - "connectable": False, + "manufacturer_id": 43605, }, { + "connectable": False, "domain": "sensorpro", - "manufacturer_id": 43605, "manufacturer_data_start": [ 1, 5, 164, 193, ], - "connectable": False, + "manufacturer_id": 43605, }, { + "connectable": False, "domain": "sensorpush", "local_name": "SensorPush*", - "connectable": False, }, { "domain": "snooz", @@ -307,65 +308,64 @@ "service_uuid": "729f0608-496a-47fe-a124-3a62aaa3fbc0", }, { + "connectable": False, "domain": "switchbot", "service_data_uuid": "00000d00-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "switchbot", "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", - "connectable": False, }, { + "connectable": False, "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b", - "connectable": False, }, { + "connectable": False, "domain": "thermobeacon", - "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", - "manufacturer_id": 16, "manufacturer_data_start": [ 0, ], - "connectable": False, + "manufacturer_id": 16, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", }, { + "connectable": False, "domain": "thermobeacon", - "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", - "manufacturer_id": 17, "manufacturer_data_start": [ 0, ], - "connectable": False, + "manufacturer_id": 17, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", }, { + "connectable": False, "domain": "thermobeacon", - "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", - "manufacturer_id": 21, "manufacturer_data_start": [ 0, ], - "connectable": False, + "manufacturer_id": 21, + "service_uuid": "0000fff0-0000-1000-8000-00805f9b34fb", }, { + "connectable": False, "domain": "thermobeacon", "local_name": "ThermoBeacon", - "connectable": False, }, { + "connectable": False, "domain": "thermopro", "local_name": "TP35*", - "connectable": False, }, { + "connectable": False, "domain": "thermopro", "local_name": "TP39*", - "connectable": False, }, { "domain": "tilt_ble", - "manufacturer_id": 76, "manufacturer_data_start": [ 2, 21, @@ -373,10 +373,11 @@ 149, 187, ], + "manufacturer_id": 76, }, { - "domain": "xiaomi_ble", "connectable": False, + "domain": "xiaomi_ble", "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb", }, { diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 46aefeebb222e5..cdcea6cf9b3383 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -1,9 +1,19 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ FLOWS = { + "helper": [ + "derivative", + "group", + "integration", + "min_max", + "switch_as_x", + "threshold", + "tod", + "utility_meter", + ], "integration": [ "abode", "accuweather", @@ -473,14 +483,4 @@ "zwave_js", "zwave_me", ], - "helper": [ - "derivative", - "group", - "integration", - "min_max", - "switch_as_x", - "threshold", - "tod", - "utility_meter", - ], } diff --git a/homeassistant/generated/currencies.py b/homeassistant/generated/currencies.py index 7a5a6a31bb5412..546bc125a0109d 100644 --- a/homeassistant/generated/currencies.py +++ b/homeassistant/generated/currencies.py @@ -1,4 +1,4 @@ -"""Automatically generated by currencies.py. +"""This file is automatically generated. To update, run python3 -m script.currencies """ diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4b8dee0d956ded..08ea9bfe64c20e 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -1,128 +1,522 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ + from __future__ import annotations DHCP: list[dict[str, str | bool]] = [ - {"domain": "august", "hostname": "connect", "macaddress": "D86162*"}, - {"domain": "august", "hostname": "connect", "macaddress": "B8B7F1*"}, - {"domain": "august", "hostname": "connect", "macaddress": "2C9FFB*"}, - {"domain": "august", "hostname": "august*", "macaddress": "E076D0*"}, - {"domain": "awair", "macaddress": "70886B1*"}, - {"domain": "axis", "registered_devices": True}, - {"domain": "axis", "hostname": "axis-00408c*", "macaddress": "00408C*"}, - {"domain": "axis", "hostname": "axis-accc8e*", "macaddress": "ACCC8E*"}, - {"domain": "axis", "hostname": "axis-b8a44f*", "macaddress": "B8A44F*"}, - {"domain": "blink", "hostname": "blink*", "macaddress": "B85F98*"}, - {"domain": "blink", "hostname": "blink*", "macaddress": "00037F*"}, - {"domain": "blink", "hostname": "blink*", "macaddress": "20A171*"}, - {"domain": "broadlink", "registered_devices": True}, - {"domain": "broadlink", "macaddress": "34EA34*"}, - {"domain": "broadlink", "macaddress": "24DFA7*"}, - {"domain": "broadlink", "macaddress": "A043B0*"}, - {"domain": "broadlink", "macaddress": "B4430D*"}, - {"domain": "broadlink", "macaddress": "C8F742*"}, - {"domain": "elkm1", "registered_devices": True}, - {"domain": "elkm1", "macaddress": "00409D*"}, - {"domain": "emonitor", "hostname": "emonitor*", "macaddress": "0090C2*"}, - {"domain": "emonitor", "registered_devices": True}, - {"domain": "esphome", "registered_devices": True}, - {"domain": "flume", "hostname": "flume-gw-*"}, - {"domain": "flux_led", "registered_devices": True}, - {"domain": "flux_led", "macaddress": "18B905*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "249494*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "7CB94C*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "ACCF23*", "hostname": "[hba][flk]*"}, - {"domain": "flux_led", "macaddress": "B4E842*", "hostname": "[ba][lk]*"}, - {"domain": "flux_led", "macaddress": "F0FE6B*", "hostname": "[hba][flk]*"}, - {"domain": "flux_led", "macaddress": "8CCE4E*", "hostname": "lwip*"}, - {"domain": "flux_led", "hostname": "hf-lpb100-zj*"}, - {"domain": "flux_led", "hostname": "zengge_[0-9a-f][0-9a-f]_*"}, - {"domain": "flux_led", "macaddress": "C82E47*", "hostname": "sta*"}, - {"domain": "fronius", "macaddress": "0003AC*"}, - {"domain": "fully_kiosk", "registered_devices": True}, - {"domain": "goalzero", "registered_devices": True}, - {"domain": "goalzero", "hostname": "yeti*"}, - {"domain": "gogogate2", "hostname": "ismartgate*"}, - {"domain": "guardian", "hostname": "gvc*", "macaddress": "30AEA4*"}, - {"domain": "guardian", "hostname": "gvc*", "macaddress": "B4E62D*"}, - {"domain": "guardian", "hostname": "guardian*", "macaddress": "30AEA4*"}, - {"domain": "hunterdouglas_powerview", "registered_devices": True}, + { + "domain": "august", + "hostname": "connect", + "macaddress": "D86162*", + }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "B8B7F1*", + }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "2C9FFB*", + }, + { + "domain": "august", + "hostname": "august*", + "macaddress": "E076D0*", + }, + { + "domain": "awair", + "macaddress": "70886B1*", + }, + { + "domain": "axis", + "registered_devices": True, + }, + { + "domain": "axis", + "hostname": "axis-00408c*", + "macaddress": "00408C*", + }, + { + "domain": "axis", + "hostname": "axis-accc8e*", + "macaddress": "ACCC8E*", + }, + { + "domain": "axis", + "hostname": "axis-b8a44f*", + "macaddress": "B8A44F*", + }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "B85F98*", + }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "00037F*", + }, + { + "domain": "blink", + "hostname": "blink*", + "macaddress": "20A171*", + }, + { + "domain": "broadlink", + "registered_devices": True, + }, + { + "domain": "broadlink", + "macaddress": "34EA34*", + }, + { + "domain": "broadlink", + "macaddress": "24DFA7*", + }, + { + "domain": "broadlink", + "macaddress": "A043B0*", + }, + { + "domain": "broadlink", + "macaddress": "B4430D*", + }, + { + "domain": "broadlink", + "macaddress": "C8F742*", + }, + { + "domain": "elkm1", + "registered_devices": True, + }, + { + "domain": "elkm1", + "macaddress": "00409D*", + }, + { + "domain": "emonitor", + "hostname": "emonitor*", + "macaddress": "0090C2*", + }, + { + "domain": "emonitor", + "registered_devices": True, + }, + { + "domain": "esphome", + "registered_devices": True, + }, + { + "domain": "flume", + "hostname": "flume-gw-*", + }, + { + "domain": "flux_led", + "registered_devices": True, + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "18B905*", + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "249494*", + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "7CB94C*", + }, + { + "domain": "flux_led", + "hostname": "[hba][flk]*", + "macaddress": "ACCF23*", + }, + { + "domain": "flux_led", + "hostname": "[ba][lk]*", + "macaddress": "B4E842*", + }, + { + "domain": "flux_led", + "hostname": "[hba][flk]*", + "macaddress": "F0FE6B*", + }, + { + "domain": "flux_led", + "hostname": "lwip*", + "macaddress": "8CCE4E*", + }, + { + "domain": "flux_led", + "hostname": "hf-lpb100-zj*", + }, + { + "domain": "flux_led", + "hostname": "zengge_[0-9a-f][0-9a-f]_*", + }, + { + "domain": "flux_led", + "hostname": "sta*", + "macaddress": "C82E47*", + }, + { + "domain": "fronius", + "macaddress": "0003AC*", + }, + { + "domain": "fully_kiosk", + "registered_devices": True, + }, + { + "domain": "goalzero", + "registered_devices": True, + }, + { + "domain": "goalzero", + "hostname": "yeti*", + }, + { + "domain": "gogogate2", + "hostname": "ismartgate*", + }, + { + "domain": "guardian", + "hostname": "gvc*", + "macaddress": "30AEA4*", + }, + { + "domain": "guardian", + "hostname": "gvc*", + "macaddress": "B4E62D*", + }, + { + "domain": "guardian", + "hostname": "guardian*", + "macaddress": "30AEA4*", + }, + { + "domain": "hunterdouglas_powerview", + "registered_devices": True, + }, { "domain": "hunterdouglas_powerview", "hostname": "hunter*", "macaddress": "002674*", }, - {"domain": "insteon", "macaddress": "000EF3*"}, - {"domain": "insteon", "registered_devices": True}, - {"domain": "intellifire", "hostname": "zentrios-*"}, - {"domain": "isy994", "registered_devices": True}, - {"domain": "isy994", "hostname": "isy*", "macaddress": "0021B9*"}, - {"domain": "isy994", "hostname": "polisy*", "macaddress": "000DB9*"}, - {"domain": "lametric", "registered_devices": True}, - {"domain": "lifx", "macaddress": "D073D5*"}, - {"domain": "lifx", "registered_devices": True}, - {"domain": "litterrobot", "hostname": "litter-robot4"}, - {"domain": "lyric", "hostname": "lyric-*", "macaddress": "48A2E6*"}, - {"domain": "lyric", "hostname": "lyric-*", "macaddress": "B82CA0*"}, - {"domain": "lyric", "hostname": "lyric-*", "macaddress": "00D02D*"}, - {"domain": "motion_blinds", "registered_devices": True}, - {"domain": "motion_blinds", "hostname": "motion_*"}, - {"domain": "motion_blinds", "hostname": "brel_*"}, - {"domain": "motion_blinds", "hostname": "connector_*"}, - {"domain": "myq", "macaddress": "645299*"}, - {"domain": "nest", "macaddress": "18B430*"}, - {"domain": "nest", "macaddress": "641666*"}, - {"domain": "nest", "macaddress": "D8EB46*"}, - {"domain": "nexia", "hostname": "xl857-*", "macaddress": "000231*"}, - {"domain": "nuheat", "hostname": "nuheat", "macaddress": "002338*"}, - {"domain": "nuki", "hostname": "nuki_bridge_*"}, - {"domain": "oncue", "hostname": "kohlergen*", "macaddress": "00146F*"}, - {"domain": "overkiz", "hostname": "gateway*", "macaddress": "F8811A*"}, - {"domain": "powerwall", "hostname": "1118431-*"}, - {"domain": "prusalink", "macaddress": "109C70*"}, - {"domain": "qnap_qsw", "macaddress": "245EBE*"}, - {"domain": "rachio", "hostname": "rachio-*", "macaddress": "009D6B*"}, - {"domain": "rachio", "hostname": "rachio-*", "macaddress": "F0038C*"}, - {"domain": "rachio", "hostname": "rachio-*", "macaddress": "74C63B*"}, - {"domain": "radiotherm", "hostname": "thermostat*", "macaddress": "5CDAD4*"}, - {"domain": "radiotherm", "registered_devices": True}, - {"domain": "rainforest_eagle", "macaddress": "D8D5B9*"}, - {"domain": "ring", "hostname": "ring*", "macaddress": "0CAE7D*"}, - {"domain": "roomba", "hostname": "irobot-*", "macaddress": "501479*"}, - {"domain": "roomba", "hostname": "roomba-*", "macaddress": "80A589*"}, - {"domain": "roomba", "hostname": "roomba-*", "macaddress": "DCF505*"}, - {"domain": "roomba", "hostname": "roomba-*", "macaddress": "204EF6*"}, - {"domain": "samsungtv", "registered_devices": True}, - {"domain": "samsungtv", "hostname": "tizen*"}, - {"domain": "samsungtv", "macaddress": "4844F7*"}, - {"domain": "samsungtv", "macaddress": "606BBD*"}, - {"domain": "samsungtv", "macaddress": "641CB0*"}, - {"domain": "samsungtv", "macaddress": "8CC8CD*"}, - {"domain": "samsungtv", "macaddress": "8CEA48*"}, - {"domain": "samsungtv", "macaddress": "F47B5E*"}, - {"domain": "screenlogic", "registered_devices": True}, - {"domain": "screenlogic", "hostname": "pentair*", "macaddress": "00C033*"}, - {"domain": "sense", "hostname": "sense-*", "macaddress": "009D6B*"}, - {"domain": "sense", "hostname": "sense-*", "macaddress": "DCEFCA*"}, - {"domain": "sense", "hostname": "sense-*", "macaddress": "A4D578*"}, - {"domain": "senseme", "registered_devices": True}, - {"domain": "senseme", "macaddress": "20F85E*"}, - {"domain": "sensibo", "hostname": "sensibo*"}, - {"domain": "simplisafe", "hostname": "simplisafe*", "macaddress": "30AEA4*"}, - {"domain": "sleepiq", "macaddress": "64DBA0*"}, - {"domain": "smartthings", "hostname": "st*", "macaddress": "24FD5B*"}, - {"domain": "smartthings", "hostname": "smartthings*", "macaddress": "24FD5B*"}, - {"domain": "smartthings", "hostname": "hub*", "macaddress": "24FD5B*"}, - {"domain": "smartthings", "hostname": "hub*", "macaddress": "D052A8*"}, - {"domain": "smartthings", "hostname": "hub*", "macaddress": "286D97*"}, - {"domain": "solaredge", "hostname": "target", "macaddress": "002702*"}, - {"domain": "somfy_mylink", "hostname": "somfy_*", "macaddress": "B8B7F1*"}, - {"domain": "squeezebox", "hostname": "squeezebox*", "macaddress": "000420*"}, - {"domain": "steamist", "registered_devices": True}, - {"domain": "steamist", "macaddress": "001E0C*", "hostname": "my[45]50*"}, - {"domain": "tado", "hostname": "tado*"}, + { + "domain": "insteon", + "macaddress": "000EF3*", + }, + { + "domain": "insteon", + "registered_devices": True, + }, + { + "domain": "intellifire", + "hostname": "zentrios-*", + }, + { + "domain": "isy994", + "registered_devices": True, + }, + { + "domain": "isy994", + "hostname": "isy*", + "macaddress": "0021B9*", + }, + { + "domain": "isy994", + "hostname": "polisy*", + "macaddress": "000DB9*", + }, + { + "domain": "lametric", + "registered_devices": True, + }, + { + "domain": "lifx", + "macaddress": "D073D5*", + }, + { + "domain": "lifx", + "registered_devices": True, + }, + { + "domain": "litterrobot", + "hostname": "litter-robot4", + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "48A2E6*", + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "B82CA0*", + }, + { + "domain": "lyric", + "hostname": "lyric-*", + "macaddress": "00D02D*", + }, + { + "domain": "motion_blinds", + "registered_devices": True, + }, + { + "domain": "motion_blinds", + "hostname": "motion_*", + }, + { + "domain": "motion_blinds", + "hostname": "brel_*", + }, + { + "domain": "motion_blinds", + "hostname": "connector_*", + }, + { + "domain": "myq", + "macaddress": "645299*", + }, + { + "domain": "nest", + "macaddress": "18B430*", + }, + { + "domain": "nest", + "macaddress": "641666*", + }, + { + "domain": "nest", + "macaddress": "D8EB46*", + }, + { + "domain": "nexia", + "hostname": "xl857-*", + "macaddress": "000231*", + }, + { + "domain": "nuheat", + "hostname": "nuheat", + "macaddress": "002338*", + }, + { + "domain": "nuki", + "hostname": "nuki_bridge_*", + }, + { + "domain": "oncue", + "hostname": "kohlergen*", + "macaddress": "00146F*", + }, + { + "domain": "overkiz", + "hostname": "gateway*", + "macaddress": "F8811A*", + }, + { + "domain": "powerwall", + "hostname": "1118431-*", + }, + { + "domain": "prusalink", + "macaddress": "109C70*", + }, + { + "domain": "qnap_qsw", + "macaddress": "245EBE*", + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "009D6B*", + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "F0038C*", + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "74C63B*", + }, + { + "domain": "radiotherm", + "hostname": "thermostat*", + "macaddress": "5CDAD4*", + }, + { + "domain": "radiotherm", + "registered_devices": True, + }, + { + "domain": "rainforest_eagle", + "macaddress": "D8D5B9*", + }, + { + "domain": "ring", + "hostname": "ring*", + "macaddress": "0CAE7D*", + }, + { + "domain": "roomba", + "hostname": "irobot-*", + "macaddress": "501479*", + }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "80A589*", + }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "DCF505*", + }, + { + "domain": "roomba", + "hostname": "roomba-*", + "macaddress": "204EF6*", + }, + { + "domain": "samsungtv", + "registered_devices": True, + }, + { + "domain": "samsungtv", + "hostname": "tizen*", + }, + { + "domain": "samsungtv", + "macaddress": "4844F7*", + }, + { + "domain": "samsungtv", + "macaddress": "606BBD*", + }, + { + "domain": "samsungtv", + "macaddress": "641CB0*", + }, + { + "domain": "samsungtv", + "macaddress": "8CC8CD*", + }, + { + "domain": "samsungtv", + "macaddress": "8CEA48*", + }, + { + "domain": "samsungtv", + "macaddress": "F47B5E*", + }, + { + "domain": "screenlogic", + "registered_devices": True, + }, + { + "domain": "screenlogic", + "hostname": "pentair*", + "macaddress": "00C033*", + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "009D6B*", + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "DCEFCA*", + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "A4D578*", + }, + { + "domain": "senseme", + "registered_devices": True, + }, + { + "domain": "senseme", + "macaddress": "20F85E*", + }, + { + "domain": "sensibo", + "hostname": "sensibo*", + }, + { + "domain": "simplisafe", + "hostname": "simplisafe*", + "macaddress": "30AEA4*", + }, + { + "domain": "sleepiq", + "macaddress": "64DBA0*", + }, + { + "domain": "smartthings", + "hostname": "st*", + "macaddress": "24FD5B*", + }, + { + "domain": "smartthings", + "hostname": "smartthings*", + "macaddress": "24FD5B*", + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "24FD5B*", + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "D052A8*", + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "286D97*", + }, + { + "domain": "solaredge", + "hostname": "target", + "macaddress": "002702*", + }, + { + "domain": "somfy_mylink", + "hostname": "somfy_*", + "macaddress": "B8B7F1*", + }, + { + "domain": "squeezebox", + "hostname": "squeezebox*", + "macaddress": "000420*", + }, + { + "domain": "steamist", + "registered_devices": True, + }, + { + "domain": "steamist", + "hostname": "my[45]50*", + "macaddress": "001E0C*", + }, + { + "domain": "tado", + "hostname": "tado*", + }, { "domain": "tesla_wall_connector", "hostname": "teslawallconnector_*", @@ -138,69 +532,296 @@ "hostname": "teslawallconnector_*", "macaddress": "4CFCAA*", }, - {"domain": "tolo", "hostname": "usr-tcp232-ed2"}, - {"domain": "toon", "hostname": "eneco-*", "macaddress": "74C63B*"}, - {"domain": "tplink", "registered_devices": True}, - {"domain": "tplink", "hostname": "es*", "macaddress": "54AF97*"}, - {"domain": "tplink", "hostname": "ep*", "macaddress": "E848B8*"}, - {"domain": "tplink", "hostname": "ep*", "macaddress": "1C61B4*"}, - {"domain": "tplink", "hostname": "ep*", "macaddress": "003192*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "1C3BF3*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "50C7BF*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "68FF7B*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "98DAC4*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "B09575*"}, - {"domain": "tplink", "hostname": "hs*", "macaddress": "C006C3*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "1C3BF3*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "50C7BF*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "68FF7B*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "98DAC4*"}, - {"domain": "tplink", "hostname": "lb*", "macaddress": "B09575*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "60A4B7*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "005F67*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "1027F5*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "B0A7B9*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "403F8C*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "C0C9E3*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "909A4A*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "E848B8*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "003192*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "1C3BF3*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "50C7BF*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "68FF7B*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "98DAC4*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "B09575*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "C006C3*"}, - {"domain": "tplink", "hostname": "k[lp]*", "macaddress": "6C5AB0*"}, - {"domain": "tuya", "macaddress": "105A17*"}, - {"domain": "tuya", "macaddress": "10D561*"}, - {"domain": "tuya", "macaddress": "1869D8*"}, - {"domain": "tuya", "macaddress": "381F8D*"}, - {"domain": "tuya", "macaddress": "508A06*"}, - {"domain": "tuya", "macaddress": "68572D*"}, - {"domain": "tuya", "macaddress": "708976*"}, - {"domain": "tuya", "macaddress": "7CF666*"}, - {"domain": "tuya", "macaddress": "84E342*"}, - {"domain": "tuya", "macaddress": "D4A651*"}, - {"domain": "tuya", "macaddress": "D81F12*"}, - {"domain": "twinkly", "hostname": "twinkly_*"}, - {"domain": "unifiprotect", "macaddress": "B4FBE4*"}, - {"domain": "unifiprotect", "macaddress": "802AA8*"}, - {"domain": "unifiprotect", "macaddress": "F09FC2*"}, - {"domain": "unifiprotect", "macaddress": "68D79A*"}, - {"domain": "unifiprotect", "macaddress": "18E829*"}, - {"domain": "unifiprotect", "macaddress": "245A4C*"}, - {"domain": "unifiprotect", "macaddress": "784558*"}, - {"domain": "unifiprotect", "macaddress": "E063DA*"}, - {"domain": "unifiprotect", "macaddress": "265A4C*"}, - {"domain": "unifiprotect", "macaddress": "74ACB9*"}, - {"domain": "verisure", "macaddress": "0023C1*"}, - {"domain": "vicare", "macaddress": "B87424*"}, - {"domain": "wiz", "registered_devices": True}, - {"domain": "wiz", "macaddress": "A8BB50*"}, - {"domain": "wiz", "macaddress": "D8A011*"}, - {"domain": "wiz", "macaddress": "444F8E*"}, - {"domain": "wiz", "macaddress": "6C2990*"}, - {"domain": "wiz", "hostname": "wiz_*"}, - {"domain": "yeelight", "hostname": "yeelink-*"}, + { + "domain": "tolo", + "hostname": "usr-tcp232-ed2", + }, + { + "domain": "toon", + "hostname": "eneco-*", + "macaddress": "74C63B*", + }, + { + "domain": "tplink", + "registered_devices": True, + }, + { + "domain": "tplink", + "hostname": "es*", + "macaddress": "54AF97*", + }, + { + "domain": "tplink", + "hostname": "ep*", + "macaddress": "E848B8*", + }, + { + "domain": "tplink", + "hostname": "ep*", + "macaddress": "1C61B4*", + }, + { + "domain": "tplink", + "hostname": "ep*", + "macaddress": "003192*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "1C3BF3*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "50C7BF*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "68FF7B*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "98DAC4*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "B09575*", + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "C006C3*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "1C3BF3*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "50C7BF*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "68FF7B*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "98DAC4*", + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "B09575*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "60A4B7*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "005F67*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "1027F5*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "B0A7B9*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "403F8C*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "C0C9E3*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "909A4A*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "E848B8*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "003192*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "1C3BF3*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "50C7BF*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "68FF7B*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "98DAC4*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "B09575*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "C006C3*", + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "6C5AB0*", + }, + { + "domain": "tuya", + "macaddress": "105A17*", + }, + { + "domain": "tuya", + "macaddress": "10D561*", + }, + { + "domain": "tuya", + "macaddress": "1869D8*", + }, + { + "domain": "tuya", + "macaddress": "381F8D*", + }, + { + "domain": "tuya", + "macaddress": "508A06*", + }, + { + "domain": "tuya", + "macaddress": "68572D*", + }, + { + "domain": "tuya", + "macaddress": "708976*", + }, + { + "domain": "tuya", + "macaddress": "7CF666*", + }, + { + "domain": "tuya", + "macaddress": "84E342*", + }, + { + "domain": "tuya", + "macaddress": "D4A651*", + }, + { + "domain": "tuya", + "macaddress": "D81F12*", + }, + { + "domain": "twinkly", + "hostname": "twinkly_*", + }, + { + "domain": "unifiprotect", + "macaddress": "B4FBE4*", + }, + { + "domain": "unifiprotect", + "macaddress": "802AA8*", + }, + { + "domain": "unifiprotect", + "macaddress": "F09FC2*", + }, + { + "domain": "unifiprotect", + "macaddress": "68D79A*", + }, + { + "domain": "unifiprotect", + "macaddress": "18E829*", + }, + { + "domain": "unifiprotect", + "macaddress": "245A4C*", + }, + { + "domain": "unifiprotect", + "macaddress": "784558*", + }, + { + "domain": "unifiprotect", + "macaddress": "E063DA*", + }, + { + "domain": "unifiprotect", + "macaddress": "265A4C*", + }, + { + "domain": "unifiprotect", + "macaddress": "74ACB9*", + }, + { + "domain": "verisure", + "macaddress": "0023C1*", + }, + { + "domain": "vicare", + "macaddress": "B87424*", + }, + { + "domain": "wiz", + "registered_devices": True, + }, + { + "domain": "wiz", + "macaddress": "A8BB50*", + }, + { + "domain": "wiz", + "macaddress": "D8A011*", + }, + { + "domain": "wiz", + "macaddress": "444F8E*", + }, + { + "domain": "wiz", + "macaddress": "6C2990*", + }, + { + "domain": "wiz", + "hostname": "wiz_*", + }, + { + "domain": "yeelight", + "hostname": "yeelink-*", + }, ] diff --git a/homeassistant/generated/mqtt.py b/homeassistant/generated/mqtt.py index 7c4203eaec22a2..4d4e47669c2ad3 100644 --- a/homeassistant/generated/mqtt.py +++ b/homeassistant/generated/mqtt.py @@ -1,4 +1,4 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index c77f3f6a68be40..210c0c832a2250 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -1,4 +1,4 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 59b59bb7604433..2d0dced8965810 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -1,14 +1,14 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ USB = [ { + "description": "*skyconnect v1.0*", "domain": "homeassistant_sky_connect", - "vid": "10C4", "pid": "EA60", - "description": "*skyconnect v1.0*", + "vid": "10C4", }, { "domain": "insteon", @@ -16,104 +16,104 @@ }, { "domain": "modem_callerid", - "vid": "0572", "pid": "1340", + "vid": "0572", }, { "domain": "velbus", - "vid": "10CF", "pid": "0B1B", + "vid": "10CF", }, { "domain": "velbus", - "vid": "10CF", "pid": "0516", + "vid": "10CF", }, { "domain": "velbus", - "vid": "10CF", "pid": "0517", + "vid": "10CF", }, { "domain": "velbus", - "vid": "10CF", "pid": "0518", + "vid": "10CF", }, { + "description": "*2652*", "domain": "zha", - "vid": "10C4", "pid": "EA60", - "description": "*2652*", + "vid": "10C4", }, { + "description": "*sonoff*plus*", "domain": "zha", - "vid": "1A86", "pid": "55D4", - "description": "*sonoff*plus*", + "vid": "1A86", }, { + "description": "*sonoff*plus*", "domain": "zha", - "vid": "10C4", "pid": "EA60", - "description": "*sonoff*plus*", + "vid": "10C4", }, { + "description": "*tubeszb*", "domain": "zha", - "vid": "10C4", "pid": "EA60", - "description": "*tubeszb*", + "vid": "10C4", }, { + "description": "*tubeszb*", "domain": "zha", - "vid": "1A86", "pid": "7523", - "description": "*tubeszb*", + "vid": "1A86", }, { + "description": "*zigstar*", "domain": "zha", - "vid": "1A86", "pid": "7523", - "description": "*zigstar*", + "vid": "1A86", }, { + "description": "*conbee*", "domain": "zha", - "vid": "1CF1", "pid": "0030", - "description": "*conbee*", + "vid": "1CF1", }, { + "description": "*zigbee*", "domain": "zha", - "vid": "10C4", "pid": "8A2A", - "description": "*zigbee*", + "vid": "10C4", }, { + "description": "*zigate*", "domain": "zha", - "vid": "0403", "pid": "6015", - "description": "*zigate*", + "vid": "0403", }, { + "description": "*zigate*", "domain": "zha", - "vid": "10C4", "pid": "EA60", - "description": "*zigate*", + "vid": "10C4", }, { + "description": "*bv 2010/10*", "domain": "zha", - "vid": "10C4", "pid": "8B34", - "description": "*bv 2010/10*", + "vid": "10C4", }, { "domain": "zwave_js", - "vid": "0658", "pid": "0200", + "vid": "0658", }, { + "description": "*z-wave*", "domain": "zwave_js", - "vid": "10C4", "pid": "8A2A", - "description": "*z-wave*", + "vid": "10C4", }, ] diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index fc0c3ea5fa7a9e..0d505bd2409da6 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -1,8 +1,70 @@ -"""Automatically generated by hassfest. +"""This file is automatically generated. To update, run python3 -m script.hassfest """ +HOMEKIT = { + "3810X": "roku", + "3820X": "roku", + "4660X": "roku", + "7820X": "roku", + "819LMB": "myq", + "AC02": "tado", + "Abode": "abode", + "BSB002": "hue", + "C105X": "roku", + "C135X": "roku", + "EB-*": "ecobee", + "Escea": "escea", + "HHKBridge*": "hive", + "Healty Home Coach": "netatmo", + "Iota": "abode", + "LIFX A19": "lifx", + "LIFX BR30": "lifx", + "LIFX Beam": "lifx", + "LIFX Candle": "lifx", + "LIFX Clean": "lifx", + "LIFX Color": "lifx", + "LIFX DLCOL": "lifx", + "LIFX DLWW": "lifx", + "LIFX Dlight": "lifx", + "LIFX Downlight": "lifx", + "LIFX Filament": "lifx", + "LIFX GU10": "lifx", + "LIFX Lightstrip": "lifx", + "LIFX Mini": "lifx", + "LIFX Nightvision": "lifx", + "LIFX Pls": "lifx", + "LIFX Plus": "lifx", + "LIFX Tile": "lifx", + "LIFX White": "lifx", + "LIFX Z": "lifx", + "MYQ": "myq", + "NL29": "nanoleaf", + "NL42": "nanoleaf", + "NL47": "nanoleaf", + "NL48": "nanoleaf", + "NL52": "nanoleaf", + "NL59": "nanoleaf", + "Netatmo Relay": "netatmo", + "PowerView": "hunterdouglas_powerview", + "Presence": "netatmo", + "Rachio": "rachio", + "SPK5": "rainmachine", + "Sensibo": "sensibo", + "Smart Bridge": "lutron_caseta", + "Socket": "wemo", + "TRADFRI": "tradfri", + "Touch HD": "rainmachine", + "Welcome": "netatmo", + "Wemo": "wemo", + "YL*": "yeelight", + "ecobee*": "ecobee", + "iSmartGate": "gogogate2", + "iZone": "izone", + "tado": "tado", +} + ZEROCONF = { "_Volumio._tcp.local.": [ { @@ -436,65 +498,3 @@ }, ], } - -HOMEKIT = { - "3810X": "roku", - "3820X": "roku", - "4660X": "roku", - "7820X": "roku", - "819LMB": "myq", - "AC02": "tado", - "Abode": "abode", - "BSB002": "hue", - "C105X": "roku", - "C135X": "roku", - "EB-*": "ecobee", - "Escea": "escea", - "HHKBridge*": "hive", - "Healty Home Coach": "netatmo", - "Iota": "abode", - "LIFX A19": "lifx", - "LIFX BR30": "lifx", - "LIFX Beam": "lifx", - "LIFX Candle": "lifx", - "LIFX Clean": "lifx", - "LIFX Color": "lifx", - "LIFX DLCOL": "lifx", - "LIFX DLWW": "lifx", - "LIFX Dlight": "lifx", - "LIFX Downlight": "lifx", - "LIFX Filament": "lifx", - "LIFX GU10": "lifx", - "LIFX Lightstrip": "lifx", - "LIFX Mini": "lifx", - "LIFX Nightvision": "lifx", - "LIFX Pls": "lifx", - "LIFX Plus": "lifx", - "LIFX Tile": "lifx", - "LIFX White": "lifx", - "LIFX Z": "lifx", - "MYQ": "myq", - "NL29": "nanoleaf", - "NL42": "nanoleaf", - "NL47": "nanoleaf", - "NL48": "nanoleaf", - "NL52": "nanoleaf", - "NL59": "nanoleaf", - "Netatmo Relay": "netatmo", - "PowerView": "hunterdouglas_powerview", - "Presence": "netatmo", - "Rachio": "rachio", - "SPK5": "rainmachine", - "Sensibo": "sensibo", - "Smart Bridge": "lutron_caseta", - "Socket": "wemo", - "TRADFRI": "tradfri", - "Touch HD": "rainmachine", - "Welcome": "netatmo", - "Wemo": "wemo", - "YL*": "yeelight", - "ecobee*": "ecobee", - "iSmartGate": "gogogate2", - "iZone": "izone", - "tado": "tado", -} diff --git a/script/currencies.py b/script/currencies.py index 2e538ff7c97472..753b3363626806 100644 --- a/script/currencies.py +++ b/script/currencies.py @@ -1,55 +1,44 @@ """Helper script to update currency list from the official source.""" -import pathlib +from pathlib import Path -import black from bs4 import BeautifulSoup import requests -BASE = """ -\"\"\"Automatically generated by currencies.py. - -To update, run python3 -m script.currencies -\"\"\" - -ACTIVE_CURRENCIES = {{ {} }} - -HISTORIC_CURRENCIES = {{ {} }} -""".strip() +from .hassfest.serializer import format_python_namespace req = requests.get( "https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml" ) soup = BeautifulSoup(req.content, "xml") -active_currencies = sorted( - { - x.Ccy.contents[0] - for x in soup.ISO_4217.CcyTbl.children - if x.name == "CcyNtry" - and x.Ccy - and x.CcyMnrUnts.contents[0] != "N.A." - and "IsFund" not in x.CcyNm.attrs - and x.Ccy.contents[0] != "UYW" - } -) +active_currencies = { + x.Ccy.contents[0] + for x in soup.ISO_4217.CcyTbl.children + if x.name == "CcyNtry" + and x.Ccy + and x.CcyMnrUnts.contents[0] != "N.A." + and "IsFund" not in x.CcyNm.attrs + and x.Ccy.contents[0] != "UYW" +} req = requests.get( "https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-three.xml" ) soup = BeautifulSoup(req.content, "xml") -historic_currencies = sorted( - { - x.Ccy.contents[0] - for x in soup.ISO_4217.HstrcCcyTbl.children - if x.name == "HstrcCcyNtry" - and x.Ccy - and "IsFund" not in x.CcyNm.attrs - and x.Ccy.contents[0] not in active_currencies - } -) - -pathlib.Path("homeassistant/generated/currencies.py").write_text( - black.format_str( - BASE.format(repr(active_currencies)[1:-1], repr(historic_currencies)[1:-1]), - mode=black.Mode(), +historic_currencies = { + x.Ccy.contents[0] + for x in soup.ISO_4217.HstrcCcyTbl.children + if x.name == "HstrcCcyNtry" + and x.Ccy + and "IsFund" not in x.CcyNm.attrs + and x.Ccy.contents[0] not in active_currencies +} + +Path("homeassistant/generated/currencies.py").write_text( + format_python_namespace( + { + "ACTIVE_CURRENCIES": active_currencies, + "HISTORIC_CURRENCIES": historic_currencies, + }, + generator="script.currencies", ) ) diff --git a/script/hassfest/application_credentials.py b/script/hassfest/application_credentials.py index 2fb693bf429752..aed8b892f50540 100644 --- a/script/hassfest/application_credentials.py +++ b/script/hassfest/application_credentials.py @@ -1,19 +1,8 @@ """Generate application_credentials data.""" from __future__ import annotations -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -APPLICATION_CREDENTIALS = {} -""".strip() +from .serializer import format_python_namespace def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: @@ -29,7 +18,7 @@ def generate_and_validate(integrations: dict[str, Integration], config: Config) match_list.append(domain) - return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) + return format_python_namespace({"APPLICATION_CREDENTIALS": match_list}) def validate(integrations: dict[str, Integration], config: Config) -> None: diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py index 0b57b1084e8411..57772edd7f4913 100644 --- a/script/hassfest/bluetooth.py +++ b/script/hassfest/bluetooth.py @@ -1,20 +1,8 @@ """Generate bluetooth file.""" from __future__ import annotations -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" -from __future__ import annotations - -BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = {} -""".strip() +from .serializer import format_python_namespace def generate_and_validate(integrations: list[dict[str, str]]): @@ -35,7 +23,10 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) + return format_python_namespace( + {"BLUETOOTH": match_list}, + annotations={"BLUETOOTH": "list[dict[str, bool | str | int | list[int]]]"}, + ) def validate(integrations: dict[str, Integration], config: Config): diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 9cebb37d3717bb..cf782a413b82f8 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -4,20 +4,9 @@ import json import pathlib -import black - from .brand import validate as validate_brands from .model import Brand, Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -FLOWS = {} -""".strip() +from .serializer import format_python_namespace UNIQUE_ID_IGNORE = {"huawei_lte", "mqtt", "adguard"} @@ -91,7 +80,7 @@ def _generate_and_validate(integrations: dict[str, Integration], config: Config) else: domains["integration"].append(domain) - return black.format_str(BASE.format(to_string(domains)), mode=black.Mode()) + return format_python_namespace({"FLOWS": domains}) def _populate_brand_integrations( diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py index c246acec5f0a71..992e1f615a18b4 100644 --- a/script/hassfest/dhcp.py +++ b/script/hassfest/dhcp.py @@ -1,19 +1,8 @@ """Generate dhcp file.""" from __future__ import annotations -import black - from .model import Config, Integration - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" -from __future__ import annotations - -DHCP: list[dict[str, str | bool]] = {} -""".strip() +from .serializer import format_python_namespace def generate_and_validate(integrations: list[dict[str, str]]): @@ -34,7 +23,10 @@ def generate_and_validate(integrations: list[dict[str, str]]): for entry in match_types: match_list.append({"domain": domain, **entry}) - return black.format_str(BASE.format(str(match_list)), mode=black.Mode()) + return format_python_namespace( + {"DHCP": match_list}, + annotations={"DHCP": "list[dict[str, str | bool]]"}, + ) def validate(integrations: dict[str, Integration], config: Config): diff --git a/script/hassfest/mqtt.py b/script/hassfest/mqtt.py index ab5f159026ed27..46ba4dfcf44803 100644 --- a/script/hassfest/mqtt.py +++ b/script/hassfest/mqtt.py @@ -3,19 +3,8 @@ from collections import defaultdict -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -MQTT = {} -""".strip() +from .serializer import format_python_namespace def generate_and_validate(integrations: dict[str, Integration]): @@ -37,7 +26,7 @@ def generate_and_validate(integrations: dict[str, Integration]): for topic in mqtt: data[domain].append(topic) - return black.format_str(BASE.format(to_string(data)), mode=black.Mode()) + return format_python_namespace({"MQTT": data}) def validate(integrations: dict[str, Integration], config: Config): diff --git a/script/hassfest/serializer.py b/script/hassfest/serializer.py index 8a6f410c345dcf..5ba386148f329a 100644 --- a/script/hassfest/serializer.py +++ b/script/hassfest/serializer.py @@ -1,37 +1,94 @@ """Hassfest utils.""" from __future__ import annotations +from collections.abc import Collection, Iterable, Mapping from typing import Any +import black -def _dict_to_str(data: dict) -> str: - """Return a string representation of a dict.""" - items = [f"'{key}':{to_string(value)}" for key, value in data.items()] - result = "{" - for item in items: - result += str(item) - result += "," - result += "}" - return result +DEFAULT_GENERATOR = "script.hassfest" -def _list_to_str(data: dict) -> str: - """Return a string representation of a list.""" - items = [to_string(value) for value in data] - result = "[" - for item in items: - result += str(item) - result += "," - result += "]" - return result +def _wrap_items( + items: Iterable[str], + opener: str, + closer: str, + sort=False, +) -> str: + """Wrap pre-formatted Python reprs in braces, optionally sorting them.""" + # The trailing comma is imperative so Black doesn't format some items + # on one line and some on multiple. + if sort: + items = sorted(items) + return f"{opener}{','.join(items)},{closer}" + + +def _mapping_to_str(data: Mapping) -> str: + """Return a string representation of a mapping.""" + return _wrap_items( + (f"{to_string(key)}:{to_string(value)}" for key, value in data.items()), + opener="{", + closer="}", + sort=True, + ) + + +def _collection_to_str( + data: Collection, opener: str = "[", closer: str = "]", sort=False +) -> str: + """Return a string representation of a collection.""" + items = (to_string(value) for value in data) + return _wrap_items(items, opener, closer, sort=sort) def to_string(data: Any) -> str: """Return a string representation of the input.""" if isinstance(data, dict): - return _dict_to_str(data) + return _mapping_to_str(data) if isinstance(data, list): - return _list_to_str(data) - if isinstance(data, str): - return "'" + data + "'" - return data + return _collection_to_str(data) + if isinstance(data, set): + return _collection_to_str(data, "{", "}", sort=True) + return repr(data) + + +def format_python( + content: str, + *, + generator: str = DEFAULT_GENERATOR, +) -> str: + """Format Python code with Black. Optionally prepend a generator comment.""" + if generator: + content = f"""\"\"\"This file is automatically generated. + +To update, run python3 -m {generator} +\"\"\" + +{content} +""" + return black.format_str(content.strip(), mode=black.Mode()) + + +def format_python_namespace( + content: dict[str, Any], + *, + annotations: dict[str, str] | None = None, + generator: str = DEFAULT_GENERATOR, +) -> str: + """Generate a nicely formatted "namespace" file. + + The keys of the `content` dict will be used as variable names. + """ + + def _get_annotation(key: str) -> str: + annotation = (annotations or {}).get(key) + return f": {annotation}" if annotation else "" + + code = "\n\n".join( + f"{key}{_get_annotation(key)}" f" = {to_string(value)}" + for key, value in sorted(content.items()) + ) + if annotations: + # If we had any annotations, add the __future__ import. + code = f"from __future__ import annotations\n{code}" + return format_python(code, generator=generator) diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 599746e9874ee7..cbe0c3ee76fec9 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -3,24 +3,8 @@ from collections import defaultdict -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -SSDP = {} -""".strip() - - -def sort_dict(value): - """Sort a dictionary.""" - return {key: value[key] for key in sorted(value)} +from .serializer import format_python_namespace def generate_and_validate(integrations: dict[str, Integration]): @@ -40,9 +24,9 @@ def generate_and_validate(integrations: dict[str, Integration]): continue for matcher in ssdp: - data[domain].append(sort_dict(matcher)) + data[domain].append(matcher) - return black.format_str(BASE.format(to_string(data)), mode=black.Mode()) + return format_python_namespace({"SSDP": data}) def validate(integrations: dict[str, Integration], config: Config): diff --git a/script/hassfest/usb.py b/script/hassfest/usb.py index e71966d548aa42..3e5ce6e3963a63 100644 --- a/script/hassfest/usb.py +++ b/script/hassfest/usb.py @@ -1,19 +1,8 @@ """Generate usb file.""" from __future__ import annotations -import black - from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -USB = {} -""".strip() +from .serializer import format_python_namespace def generate_and_validate(integrations: list[dict[str, str]]) -> str: @@ -39,7 +28,7 @@ def generate_and_validate(integrations: list[dict[str, str]]) -> str: } ) - return black.format_str(BASE.format(to_string(match_list)), mode=black.Mode()) + return format_python_namespace({"USB": match_list}) def validate(integrations: dict[str, Integration], config: Config) -> None: diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 939da08319adf7..0c372035bdcc40 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -3,23 +3,10 @@ from collections import defaultdict -import black - from homeassistant.loader import async_process_zeroconf_match_dict from .model import Config, Integration -from .serializer import to_string - -BASE = """ -\"\"\"Automatically generated by hassfest. - -To update, run python3 -m script.hassfest -\"\"\" - -ZEROCONF = {} - -HOMEKIT = {} -""".strip() +from .serializer import format_python_namespace def generate_and_validate(integrations: dict[str, Integration]): @@ -82,11 +69,11 @@ def generate_and_validate(integrations: dict[str, Integration]): warned.add(key_2) break - zeroconf = {key: service_type_dict[key] for key in sorted(service_type_dict)} - homekit = {key: homekit_dict[key] for key in sorted(homekit_dict)} - - return black.format_str( - BASE.format(to_string(zeroconf), to_string(homekit)), mode=black.Mode() + return format_python_namespace( + { + "HOMEKIT": {key: homekit_dict[key] for key in homekit_dict}, + "ZEROCONF": {key: service_type_dict[key] for key in service_type_dict}, + } ) From 438184a43cf212d05db94a560269267324d65c9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 10:01:12 -0600 Subject: [PATCH 350/394] Bump aiohomekit to 2.2.19 (#81867) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index b18f35390b7985..f0438a7b841fa7 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==2.2.18"], + "requirements": ["aiohomekit==2.2.19"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index d0951302aff887..d939123b1a999a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -174,7 +174,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.18 +aiohomekit==2.2.19 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b34ef5074d4227..a52ca259d5ede2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,7 +158,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==2.2.18 +aiohomekit==2.2.19 # homeassistant.components.emulated_hue # homeassistant.components.http From b18c558a803e58a638c56a375ca46b0a81e7f194 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 9 Nov 2022 10:21:31 -0600 Subject: [PATCH 351/394] Bump oralb-ble to 0.14.1 (#81869) --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 1738558770e86b..eff6c999c30fa3 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.13.0"], + "requirements": ["oralb-ble==0.14.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index d939123b1a999a..2fa5e7d4f8bcc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1255,7 +1255,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.13.0 +oralb-ble==0.14.1 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a52ca259d5ede2..07a299c70b83d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -900,7 +900,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.13.0 +oralb-ble==0.14.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 0941ed076ccafe36e2e8ffbdc480cea4f8fa5650 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 9 Nov 2022 11:59:00 -0800 Subject: [PATCH 352/394] Cleanup unnecessary google calendar test fixtures (#81876) --- tests/components/google/test_calendar.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 90ec8f44850ac6..79eff393cc7ae5 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -301,15 +301,12 @@ async def test_missing_summary(hass, mock_events_list_items, component_setup): async def test_update_error( hass, component_setup, - mock_calendars_list, mock_events_list, - test_api_calendar, aioclient_mock, ): """Test that the calendar update handles a server error.""" now = dt_util.now() - mock_calendars_list({"items": [test_api_calendar]}) mock_events_list( { "items": [ @@ -516,7 +513,6 @@ async def test_opaque_event( async def test_scan_calendar_error( hass, component_setup, - test_api_calendar, mock_calendars_list, config_entry, ): From adf84b0c62c4d6472d589b086162563f4f27a854 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 9 Nov 2022 15:36:50 -0700 Subject: [PATCH 353/394] Add `async_get_active_reauth_flows` helper for config entries (#81881) * Add `async_get_active_reauth_flows` helper for config entries * Code review * Code review + tests --- .../components/openuv/coordinator.py | 21 ++++-------- homeassistant/config_entries.py | 22 +++++++++---- tests/test_config_entries.py | 32 +++++++++++++++++++ 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/openuv/coordinator.py b/homeassistant/components/openuv/coordinator.py index 36267972f80481..1df6a91b3982a7 100644 --- a/homeassistant/components/openuv/coordinator.py +++ b/homeassistant/components/openuv/coordinator.py @@ -7,13 +7,13 @@ from pyopenuv.errors import InvalidApiKeyError, OpenUvError -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, LOGGER +from .const import LOGGER DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60 @@ -56,19 +56,10 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: @callback def _get_active_reauth_flow(self) -> FlowResult | None: """Get an active reauth flow (if it exists).""" - try: - [reauth_flow] = [ - flow - for flow in self.hass.config_entries.flow.async_progress_by_handler( - DOMAIN - ) - if flow["context"]["source"] == "reauth" - and flow["context"]["entry_id"] == self.entry.entry_id - ] - except ValueError: - return None - - return reauth_flow + return next( + iter(self.entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})), + None, + ) @callback def cancel_reauth(self) -> None: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 902fa0d03f2c1c..ddef5d7f226cb4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,7 +3,7 @@ import asyncio from collections import ChainMap -from collections.abc import Callable, Coroutine, Iterable, Mapping +from collections.abc import Callable, Coroutine, Generator, Iterable, Mapping from contextvars import ContextVar from enum import Enum import functools @@ -19,6 +19,7 @@ from .components import persistent_notification from .const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, Platform from .core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback +from .data_entry_flow import FlowResult from .exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady, HomeAssistantError from .helpers import device_registry, entity_registry, storage from .helpers.dispatcher import async_dispatcher_connect, async_dispatcher_send @@ -662,12 +663,7 @@ def async_start_reauth( data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" - if any( - flow - for flow in hass.config_entries.flow.async_progress_by_handler(self.domain) - if flow["context"].get("source") == SOURCE_REAUTH - and flow["context"].get("entry_id") == self.entry_id - ): + if any(self.async_get_active_flows(hass, {SOURCE_REAUTH})): # Reauth flow already in progress for this entry return @@ -685,6 +681,18 @@ def async_start_reauth( ) ) + @callback + def async_get_active_flows( + self, hass: HomeAssistant, sources: set[str] + ) -> Generator[FlowResult, None, None]: + """Get any active flows of certain sources for this entry.""" + return ( + flow + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain) + if flow["context"].get("source") in sources + and flow["context"].get("entry_id") == self.entry_id + ) + @callback def async_create_task( self, hass: HomeAssistant, target: Coroutine[Any, Any, _R] diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 96d032f771e6c1..28c3f9c2803929 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3429,3 +3429,35 @@ async def _load_entry() -> None: }, timeout=0.1, ) + + +async def test_get_active_flows(hass): + """Test the async_get_active_flows helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init): + entry.async_start_reauth( + hass, + context={"extra_context": "some_extra_context"}, + data={"extra_data": 1234}, + ) + await hass.async_block_till_done() + + # Check that there's an active reauth flow: + active_reauth_flow = next( + iter(entry.async_get_active_flows(hass, {config_entries.SOURCE_REAUTH})), None + ) + assert active_reauth_flow is not None + + # Check that there isn't any other flow (in this case, a user flow): + active_user_flow = next( + iter(entry.async_get_active_flows(hass, {config_entries.SOURCE_USER})), None + ) + assert active_user_flow is None From 7aa4654eb499c3c1f50179f108017c62374a2e47 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Nov 2022 23:59:58 +0100 Subject: [PATCH 354/394] Improve docstring for sensor testcase (#81875) --- tests/components/sensor/test_recorder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 05f8bd40597d18..f8bdf7acf87f65 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1729,7 +1729,11 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): def test_compile_hourly_statistics_unavailable( hass_recorder, caplog, device_class, state_unit, value ): - """Test compiling hourly statistics, with the sensor being unavailable.""" + """Test compiling hourly statistics, with one sensor being unavailable. + + sensor.test1 is unavailable and should not have statistics generated + sensor.test2 should have statistics generated + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) From 8b4dbbe593d1c37ebef5678340d8876ded987bed Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 10 Nov 2022 00:27:41 +0000 Subject: [PATCH 355/394] [ci skip] Translation update --- .../components/august/translations/bg.json | 2 +- .../components/blink/translations/bg.json | 2 +- .../components/braviatv/translations/bg.json | 6 ++-- .../components/deconz/translations/bg.json | 18 +++++----- .../devolo_home_network/translations/bg.json | 2 +- .../google_travel_time/translations/bg.json | 2 +- .../homeassistant/translations/de.json | 6 ++++ .../homeassistant/translations/id.json | 6 ++++ .../huawei_lte/translations/bg.json | 4 +-- .../components/hue/translations/bg.json | 10 ++++++ .../components/lametric/translations/bg.json | 2 +- .../components/livisi/translations/ca.json | 16 +++++++++ .../components/livisi/translations/id.json | 18 ++++++++++ .../components/mqtt/translations/bg.json | 13 +++++++ .../components/mqtt/translations/id.json | 10 +++--- .../components/mqtt/translations/ru.json | 2 +- .../components/nam/translations/bg.json | 2 +- .../nibe_heatpump/translations/ca.json | 12 ++++++- .../nibe_heatpump/translations/de.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/id.json | 27 ++++++++++++++- .../nibe_heatpump/translations/no.json | 34 ++++++++++++++++++- .../nibe_heatpump/translations/zh-Hant.json | 34 ++++++++++++++++++- .../components/openuv/translations/bg.json | 9 ++++- .../components/openuv/translations/ca.json | 10 +++++- .../components/openuv/translations/de.json | 10 +++++- .../components/openuv/translations/id.json | 10 +++++- .../components/openuv/translations/no.json | 10 +++++- .../components/openuv/translations/pl.json | 10 +++++- .../openuv/translations/zh-Hant.json | 10 +++++- .../ovo_energy/translations/bg.json | 2 +- .../components/ring/translations/bg.json | 2 +- .../components/sensor/translations/bg.json | 2 +- .../components/shelly/translations/bg.json | 2 +- .../components/subaru/translations/bg.json | 2 +- .../totalconnect/translations/bg.json | 2 +- .../translations/bg.json | 2 +- .../unifiprotect/translations/ca.json | 4 +-- .../components/zha/translations/bg.json | 34 +++++++++++-------- 38 files changed, 323 insertions(+), 60 deletions(-) create mode 100644 homeassistant/components/livisi/translations/ca.json create mode 100644 homeassistant/components/livisi/translations/id.json diff --git a/homeassistant/components/august/translations/bg.json b/homeassistant/components/august/translations/bg.json index 7c3899abed7f80..110c7ee5598225 100644 --- a/homeassistant/components/august/translations/bg.json +++ b/homeassistant/components/august/translations/bg.json @@ -11,7 +11,7 @@ } }, "validation": { - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/blink/translations/bg.json b/homeassistant/components/blink/translations/bg.json index 60e7c86f621b9c..c302ce972b9695 100644 --- a/homeassistant/components/blink/translations/bg.json +++ b/homeassistant/components/blink/translations/bg.json @@ -14,7 +14,7 @@ "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u041f\u0418\u041d \u043a\u043e\u0434\u0430, \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0438\u043c\u0435\u0439\u043b", - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index 3a6908b01770cc..8df7ac6e1c3838 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -16,7 +16,7 @@ "authorize": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } }, "confirm": { @@ -25,7 +25,7 @@ "reauth_confirm": { "data": { "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } }, "user": { diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index b047c3616813bc..8b9ae63a7cd263 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -31,6 +31,7 @@ "device_automation": { "trigger_subtype": { "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "bottom_buttons": "\u0414\u043e\u043b\u043d\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", @@ -51,21 +52,22 @@ "side_4": "\u0421\u0442\u0440\u0430\u043d\u0430 4", "side_5": "\u0421\u0442\u0440\u0430\u043d\u0430 5", "side_6": "\u0421\u0442\u0440\u0430\u043d\u0430 6", + "top_buttons": "\u0413\u043e\u0440\u043d\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" }, "trigger_type": { "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u0435 \u0441\u044a\u0431\u0443\u0434\u0438", - "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", - "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", - "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", "remote_button_rotation_stopped": "\u0421\u043f\u0440\u044f \u0432\u044a\u0440\u0442\u0435\u043d\u0435\u0442\u043e \u043d\u0430 \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", - "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", - "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", - "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "remote_button_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \"{subtype}\" \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \u0434\u0432\u0430 \u043f\u044a\u0442\u0438", "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043f\u0430\u0434\u0430", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", diff --git a/homeassistant/components/devolo_home_network/translations/bg.json b/homeassistant/components/devolo_home_network/translations/bg.json index a90c099889a9b8..44d409938a0ba8 100644 --- a/homeassistant/components/devolo_home_network/translations/bg.json +++ b/homeassistant/components/devolo_home_network/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json index 3965f9c706cc0f..b599c0d4dedab8 100644 --- a/homeassistant/components/google_travel_time/translations/bg.json +++ b/homeassistant/components/google_travel_time/translations/bg.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "user": { diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index 756670e5a47f70..61ca66f49a9178 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "Die W\u00e4hrung {currency} wird nicht mehr verwendet, bitte konfiguriere die W\u00e4hrungskonfiguration neu.", + "title": "Die konfigurierte W\u00e4hrung ist nicht mehr in Gebrauch" + } + }, "system_health": { "info": { "arch": "CPU-Architektur", diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json index 7c2994d8bbb298..0be9cd9f286b0c 100644 --- a/homeassistant/components/homeassistant/translations/id.json +++ b/homeassistant/components/homeassistant/translations/id.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "Mata uang {currency} tidak lagi digunakan, konfigurasikan ulang konfigurasi mata uang.", + "title": "Mata uang yang dikonfigurasi tidak lagi digunakan" + } + }, "system_health": { "info": { "arch": "Arsitektur CPU", diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 8f34e808235fc9..2ecb9564113683 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", @@ -20,7 +20,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { "data": { diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 242c902fde51e0..ae6ce66fbc0944 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -39,13 +39,23 @@ "2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "clock_wise": "\u0412\u044a\u0440\u0442\u0435\u043d\u0435 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430", "counter_clock_wise": "\u0412\u044a\u0440\u0442\u0435\u043d\u0435 \u043e\u0431\u0440\u0430\u0442\u043d\u043e \u043d\u0430 \u0447\u0430\u0441\u043e\u0432\u043d\u0438\u043a\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0440\u0435\u043b\u043a\u0430", "double_buttons_1_3": "\u041f\u044a\u0440\u0432\u0438 \u0438 \u0442\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438", "double_buttons_2_4": "\u0412\u0442\u043e\u0440\u0438 \u0438 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d\u0438" }, "trigger_type": { + "long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "remote_double_button_long_press": "\u0418 \u0434\u0432\u0430\u0442\u0430 \"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_double_button_short_press": "\u0418 \u0434\u0432\u0430\u0442\u0430 \"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043a\u0440\u0430\u0442\u043a\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", "start": "\"{subtype}\" \u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u044a\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u043d\u043e" } }, diff --git a/homeassistant/components/lametric/translations/bg.json b/homeassistant/components/lametric/translations/bg.json index 6f85c1ddaf5cd6..a886ec4fe9afb7 100644 --- a/homeassistant/components/lametric/translations/bg.json +++ b/homeassistant/components/lametric/translations/bg.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { diff --git a/homeassistant/components/livisi/translations/ca.json b/homeassistant/components/livisi/translations/ca.json new file mode 100644 index 00000000000000..359b7eb01b1b4a --- /dev/null +++ b/homeassistant/components/livisi/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "wrong_password": "La contrasenya \u00e9s incorrecta." + }, + "step": { + "user": { + "data": { + "host": "Adre\u00e7a IP", + "password": "Contrasenya" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/id.json b/homeassistant/components/livisi/translations/id.json new file mode 100644 index 00000000000000..8be49b10d35865 --- /dev/null +++ b/homeassistant/components/livisi/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "wrong_ip_address": "Alamat IP salah atau SHC tidak dapat dihubungi secara lokal.", + "wrong_password": "Kata sandi salah." + }, + "step": { + "user": { + "data": { + "host": "Alamat IP", + "password": "Kata Sandi" + }, + "description": "Masukkan alamat IP dan kata sandi (lokal) SHC." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index 06751555b295b1..c3dd9f6ec2fa3d 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -35,9 +35,22 @@ }, "device_automation": { "trigger_subtype": { + "button_1": "\u041f\u044a\u0440\u0432\u043e \u043a\u043e\u043f\u0447\u0435", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_5": "\u041f\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "button_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435" } }, "options": { diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index c6c67cea9c7328..3f2a6cb18cb832 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Opsi tingkat lanjut", "broker": "Broker", - "certificate": "Jalur ke file sertifikat CA khusus", - "client_cert": "Jalur ke file sertifikat klien", + "certificate": "Unggah file sertifikat CA khusus", + "client_cert": "Unggah file sertifikat klien", "client_id": "ID Klien (biarkan kosong agar dihasilkan secara acak)", - "client_key": "Jalur ke file kunci pribadi", + "client_key": "Unggah file kunci pribadi", "discovery": "Aktifkan penemuan", "keepalive": "Waktu antara mengirim pesan tetap hidup", "password": "Kata Sandi", @@ -96,9 +96,9 @@ "broker": "Broker", "certificate": "Unggah file sertifikat CA khusus", "client_cert": "Unggah file sertifikat klien", - "client_id": "ID Klien (biarkan kosong agar dibuat secara acak)", + "client_id": "ID Klien (biarkan kosong agar dihasilkan secara acak)", "client_key": "Unggah file kunci pribadi", - "keepalive": "Waktu antara mengirim pesan keep alive", + "keepalive": "Waktu antara mengirim pesan tetap hidup", "password": "Kata Sandi", "port": "Port", "protocol": "Protokol MQTT", diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 7e2fe9ffe55574..e0de1f14936925 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -113,7 +113,7 @@ "birth_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)", - "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", + "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432", "discovery_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", "will_enable": "\u041e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json index 9be1a75603acbd..57ae4ea05d0d83 100644 --- a/homeassistant/components/nam/translations/bg.json +++ b/homeassistant/components/nam/translations/bg.json @@ -4,7 +4,7 @@ "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "device_unsupported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", diff --git a/homeassistant/components/nibe_heatpump/translations/ca.json b/homeassistant/components/nibe_heatpump/translations/ca.json index d2924212386292..7dcf70cc17d27b 100644 --- a/homeassistant/components/nibe_heatpump/translations/ca.json +++ b/homeassistant/components/nibe_heatpump/translations/ca.json @@ -12,6 +12,12 @@ "write": "Error en la sol\u00b7licitud d'escriptura a la bomba. Verifica el port remot d'escriptura i/o l'adre\u00e7a IP remota." }, "step": { + "modbus": { + "data": { + "modbus_url": "URL de Modbus", + "model": "Model de bomba de calor" + } + }, "user": { "data": { "ip_address": "Adre\u00e7a remota", @@ -25,7 +31,11 @@ "remote_read_port": "Port on la unitat NibeGW espera les sol\u00b7licituds de lectura.", "remote_write_port": "Port on la unitat NibeGW espera les sol\u00b7licituds d'escriptura." }, - "description": "Abans d'intentar configurar la integraci\u00f3, comprova que:\n - La unitat NibeGW est\u00e0 connectada a una bomba de calor.\n - S'ha activat l'accessori MODBUS40 a la configuraci\u00f3 de la bomba de calor.\n - La bomba no ha entrat en estat d'alarma per falta de l'accessori MODBUS40." + "description": "Abans d'intentar configurar la integraci\u00f3, comprova que:\n - La unitat NibeGW est\u00e0 connectada a una bomba de calor.\n - S'ha activat l'accessori MODBUS40 a la configuraci\u00f3 de la bomba de calor.\n - La bomba no ha entrat en estat d'alarma per falta de l'accessori MODBUS40.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/de.json b/homeassistant/components/nibe_heatpump/translations/de.json index 5cddee9d912d84..e5f98b60bddf1d 100644 --- a/homeassistant/components/nibe_heatpump/translations/de.json +++ b/homeassistant/components/nibe_heatpump/translations/de.json @@ -9,13 +9,26 @@ "model": "Das ausgew\u00e4hlte Modell scheint modbus40 nicht zu unterst\u00fctzen", "read": "Fehler bei Leseanforderung von Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Leseport\u201c oder \u201eRemote-IP-Adresse\u201c.", "unknown": "Unerwarteter Fehler", + "url": "Die angegebene URL ist keine wohlgeformte und unterst\u00fctzte URL", "write": "Fehler bei Schreibanforderung an Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Schreibport\u201c oder \u201eRemote-IP-Adresse\u201c." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus-Einheitenkennung", + "modbus_url": "Modbus-URL", + "model": "Modell der W\u00e4rmepumpe" + }, + "data_description": { + "modbus_unit": "Ger\u00e4teidentifikation f\u00fcr deine W\u00e4rmepumpe. Kann normalerweise auf 0 belassen werden.", + "modbus_url": "Modbus-URL, die die Verbindung zu deiner W\u00e4rmepumpe oder deinem MODBUS40-Ger\u00e4t beschreibt. Sie sollte in folgender Form sein:\n - `tcp://[HOST]:[PORT]` f\u00fcr eine Modbus TCP-Verbindung\n - `serial://[LOKALES GER\u00c4T]` f\u00fcr eine lokale Modbus RTU-Verbindung\n - `rfc2217://[HOST]:[PORT]` f\u00fcr eine Telnet-basierte Modbus-RTU-Fernverbindung." + } + }, + "nibegw": { "data": { "ip_address": "Remote-Adresse", "listening_port": "Lokaler Leseport", + "model": "Modell der W\u00e4rmepumpe", "remote_read_port": "Remote-Leseport", "remote_write_port": "Remote-Schreibport" }, @@ -26,6 +39,25 @@ "remote_write_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Schreibanfragen wartet." }, "description": "Bevor du versuchst, die Integration zu konfigurieren, \u00fcberpr\u00fcfe folgendes:\n - Das NibeGW-Ger\u00e4t ist an eine W\u00e4rmepumpe angeschlossen.\n - Das MODBUS40-Zubeh\u00f6r wurde in der Konfiguration der W\u00e4rmepumpe aktiviert.\n - Die Pumpe ist nicht in einen Alarmzustand wegen fehlendem MODBUS40-Zubeh\u00f6r \u00fcbergegangen." + }, + "user": { + "data": { + "ip_address": "Remote-Adresse", + "listening_port": "Lokaler Leseport", + "remote_read_port": "Remote-Leseport", + "remote_write_port": "Remote-Schreibport" + }, + "data_description": { + "ip_address": "Die Adresse des NibeGW-Ger\u00e4ts. Das Ger\u00e4t sollte mit einer statischen Adresse konfiguriert worden sein.", + "listening_port": "Der lokale Port auf diesem System, an den das NibeGW-Ger\u00e4t Daten senden soll.", + "remote_read_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Leseanfragen wartet.", + "remote_write_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Schreibanfragen wartet." + }, + "description": "W\u00e4hle die Verbindungsmethode zu deiner Pumpe. Im Allgemeinen erfordern Pumpen der F-Serie ein kundenspezifisches Nibe GW-Zubeh\u00f6r, w\u00e4hrend eine Pumpe der S-Serie \u00fcber eine integrierte Modbus-Unterst\u00fctzung verf\u00fcgt.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/id.json b/homeassistant/components/nibe_heatpump/translations/id.json index 3ee3210ee768d1..44e0a8c024732d 100644 --- a/homeassistant/components/nibe_heatpump/translations/id.json +++ b/homeassistant/components/nibe_heatpump/translations/id.json @@ -12,6 +12,27 @@ "write": "Kesalahan pada permintaan tulis ke pompa. Verifikasi `Port tulis jarak jauh` atau `Alamat IP jarak jauh` Anda." }, "step": { + "modbus": { + "data": { + "modbus_unit": "Pengidentifikasi Unit Modbus", + "modbus_url": "URL Modbus", + "model": "Model Pompa Panas" + } + }, + "nibegw": { + "data": { + "ip_address": "Alamat jarak jauh", + "listening_port": "Port mendengarkan lokal", + "model": "Model Pompa Panas", + "remote_read_port": "Port baca jarak jauh", + "remote_write_port": "Port tulis jarak jauh" + }, + "data_description": { + "ip_address": "Alamat unit NibeGW. Perangkat seharusnya sudah dikonfigurasi dengan alamat statis.", + "listening_port": "Port lokal pada sistem ini, yang dikonfigurasi untuk mengirim data ke unit NibeGW." + }, + "description": "Sebelum mencoba mengonfigurasi integrasi, pastikan bahwa:\n - Unit NibeGW terhubung ke pompa panas.\n - Aksesori MODBUS40 telah diaktifkan dalam konfigurasi pompa panas.\n - Pompa tidak sedang dalam status alarm tentang aksesori MODBUS40 yang tidak tersedia." + }, "user": { "data": { "ip_address": "Alamat jarak jauh", @@ -25,7 +46,11 @@ "remote_read_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan baca.", "remote_write_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan tulis." }, - "description": "Sebelum mencoba mengkonfigurasi integrasi, pastikan bahwa:\n- Unit NibeGW terhubung ke pompa pemanas.\n- Aksesori MODBUS40 telah diaktifkan dalam konfigurasi pompa pemanas.\n- Pompa belum masuk ke dalam kondisi alarm tentang ketiadaan aksesori MODBUS40." + "description": "Pilih metode koneksi ke pompa. Secara umum, pompa seri F memerlukan aksesori khusus Nibe GW, sementara pompa seri S memiliki dukungan Modbus bawaan.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/no.json b/homeassistant/components/nibe_heatpump/translations/no.json index b0a8f601776435..e2ffa8790f7531 100644 --- a/homeassistant/components/nibe_heatpump/translations/no.json +++ b/homeassistant/components/nibe_heatpump/translations/no.json @@ -9,13 +9,26 @@ "model": "Den valgte modellen ser ikke ut til \u00e5 st\u00f8tte modbus40", "read": "Feil ved leseforesp\u00f8rsel fra pumpe. Bekreft din \"Ekstern leseport\" eller \"Ekstern IP-adresse\".", "unknown": "Uventet feil", + "url": "Den spesifiserte nettadressen er ikke en godt utformet og st\u00f8ttet nettadresse", "write": "Feil ved skriveforesp\u00f8rsel til pumpen. Bekreft din \"Ekstern skriveport\" eller \"Ekstern IP-adresse\"." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus Unit Identifier", + "modbus_url": "Modbus URL", + "model": "Modell av varmepumpe" + }, + "data_description": { + "modbus_unit": "Enhetsidentifikasjon for din varmepumpe. Kan vanligvis st\u00e5 p\u00e5 0.", + "modbus_url": "Modbus URL som beskriver tilkoblingen til din varmepumpe eller MODBUS40 enhet. Det skal st\u00e5 p\u00e5 skjemaet:\n - `tcp://[HOST]:[PORT]` for Modbus TCP-tilkobling\n - `serial://[LOCAL DEVICE]` for en lokal Modbus RTU-tilkobling\n - `rfc2217://[HOST]:[PORT]` for en ekstern telnet-basert Modbus RTU-tilkobling." + } + }, + "nibegw": { "data": { "ip_address": "Ekstern adresse", "listening_port": "Lokal lytteport", + "model": "Modell av varmepumpe", "remote_read_port": "Ekstern leseport", "remote_write_port": "Ekstern skriveport" }, @@ -26,6 +39,25 @@ "remote_write_port": "Porten NibeGW-enheten lytter etter skriveforesp\u00f8rsler p\u00e5." }, "description": "F\u00f8r du pr\u00f8ver \u00e5 konfigurere integrasjonen, kontroller at:\n - NibeGW-enheten er koblet til en varmepumpe.\n - MODBUS40-tilbeh\u00f8ret er aktivert i varmepumpekonfigurasjonen.\n - Pumpen har ikke g\u00e5tt i alarmtilstand om manglende MODBUS40-tilbeh\u00f8r." + }, + "user": { + "data": { + "ip_address": "Ekstern adresse", + "listening_port": "Lokal lytteport", + "remote_read_port": "Ekstern leseport", + "remote_write_port": "Ekstern skriveport" + }, + "data_description": { + "ip_address": "Adressen til NibeGW-enheten. Enheten skal ha blitt konfigurert med en statisk adresse.", + "listening_port": "Den lokale porten p\u00e5 dette systemet, som NibeGW-enheten er konfigurert til \u00e5 sende data til.", + "remote_read_port": "Porten NibeGW-enheten lytter etter leseforesp\u00f8rsler p\u00e5.", + "remote_write_port": "Porten NibeGW-enheten lytter etter skriveforesp\u00f8rsler p\u00e5." + }, + "description": "Velg tilkoblingsmetoden til pumpen din. Generelt krever pumper i F-serien et Nibe GW-tilpasset tilbeh\u00f8r, mens en pumpe i S-serien har Modbus-st\u00f8tte innebygd.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json index c00ea79de1cf23..fe182397efa4da 100644 --- a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json +++ b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json @@ -9,13 +9,26 @@ "model": "\u6240\u9078\u64c7\u7684\u578b\u865f\u4f3c\u4e4e\u4e0d\u652f\u63f4 modbus40", "read": "\u8b80\u53d6\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u8b80\u53d6\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "url": "\u6307\u5b9a\u7684 URL \u4e0d\u662f\u6b63\u78ba\u683c\u5f0f\u6216\u652f\u63f4\u7684URL", "write": "\u5beb\u5165\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u5beb\u5165\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002" }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus \u8a2d\u5099\u8b58\u5225", + "modbus_url": "Modbus URL", + "model": "\u71b1\u6cf5\u578b\u865f" + }, + "data_description": { + "modbus_unit": "\u71b1\u6cf5\u8a2d\u5099\u8b58\u5225\uff0c\u901a\u5e38\u53ef\u4ee5\u4fdd\u7559\u70ba 0\u3002", + "modbus_url": "Modbus URL \u70ba\u9023\u7dda\u81f3\u71b1\u6cf5\u6216 MODBUS40 \u8a2d\u5099\u4e4b\u5167\u5bb9\u3001\u61c9\u8a72\u70ba\u4e0b\u5217\u683c\u5f0f\uff1a\n - `tcp://[HOST]:[PORT]` \u7528\u70ba Modbus TCP \u9023\u7dda\n - `serial://[LOCAL DEVICE]` \u7528\u70ba\u672c\u5730\u7aef Modbus RTU \u9023\u7dda\n - `rfc2217://[HOST]:[PORT]` \u7528\u70ba\u9060\u7aef telnet \u985e\u578b Modbus RTU \u9023\u7dda\u3002" + } + }, + "nibegw": { "data": { "ip_address": "\u9060\u7aef\u4f4d\u5740", "listening_port": "\u672c\u5730\u76e3\u807d\u901a\u8a0a\u57e0", + "model": "\u71b1\u6cf5\u578b\u865f", "remote_read_port": "\u9060\u7aef\u8b80\u53d6\u57e0", "remote_write_port": "\u9060\u7aef\u5beb\u5165\u57e0" }, @@ -26,6 +39,25 @@ "remote_write_port": "NibeGW \u8a2d\u5099\u76e3\u807d\u5beb\u5165\u8acb\u6c42\u901a\u8a0a\u57e0\u3002" }, "description": "\u65bc\u5617\u8a66\u8a2d\u5b9a\u6574\u5408\u524d\u3001\u8acb\u78ba\u8a8d\uff1a\n - NibeGW \u8a2d\u5099\u5df2\u7d93\u9023\u7dda\u81f3\u71b1\u6cf5\u3002\n - \u5df2\u7d93\u65bc\u71b1\u6cf5\u8a2d\u5b9a\u4e2d\u555f\u7528 MODBUS40 \u914d\u4ef6\u3002\n - \u6cf5\u4e26\u6c92\u6709\u51fa\u73fe\u7f3a\u5c11 MODBUS40 \u914d\u4ef6\u4e4b\u8b66\u544a\u3002" + }, + "user": { + "data": { + "ip_address": "\u9060\u7aef\u4f4d\u5740", + "listening_port": "\u672c\u5730\u76e3\u807d\u901a\u8a0a\u57e0", + "remote_read_port": "\u9060\u7aef\u8b80\u53d6\u57e0", + "remote_write_port": "\u9060\u7aef\u5beb\u5165\u57e0" + }, + "data_description": { + "ip_address": "NibeGW \u8a2d\u5099\u4f4d\u5740\u3002\u88dd\u7f6e\u61c9\u8a72\u5df2\u7d93\u8a2d\u5b9a\u70ba\u975c\u614b\u4f4d\u5740\uff0c", + "listening_port": "\u7cfb\u7d71\u672c\u5730\u901a\u8a0a\u57e0\u3001\u4f9b NibeGW \u8a2d\u5099\u8a2d\u5b9a\u50b3\u9001\u8cc7\u6599\u3002", + "remote_read_port": "NibeGW \u8a2d\u5099\u76e3\u807d\u8b80\u53d6\u8acb\u6c42\u901a\u8a0a\u57e0\u3002", + "remote_write_port": "NibeGW \u8a2d\u5099\u76e3\u807d\u5beb\u5165\u8acb\u6c42\u901a\u8a0a\u57e0\u3002" + }, + "description": "\u9078\u64c7\u6cf5\u9023\u7dda\u6a21\u5f0f\u3002\u901a\u5e38\u3001F \u7cfb\u5217\u6cf5\u9700\u8981 Nibe GW \u81ea\u8a02\u914d\u4ef6\u3001\u800c S \u7cfb\u5217\u6cf5\u70ba\u5167\u5efa Modbus\u3002", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/openuv/translations/bg.json b/homeassistant/components/openuv/translations/bg.json index 6959a04bb7a752..06c8d9cde09377 100644 --- a/homeassistant/components/openuv/translations/bg.json +++ b/homeassistant/components/openuv/translations/bg.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447 \u0437\u0430 OpenUV", diff --git a/homeassistant/components/openuv/translations/ca.json b/homeassistant/components/openuv/translations/ca.json index 36043c3bde3d3c..b7a7f3e7630bd1 100644 --- a/homeassistant/components/openuv/translations/ca.json +++ b/homeassistant/components/openuv/translations/ca.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_api_key": "Clau API inv\u00e0lida" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "Si us plau, torna a introduir la clau API de {latitude}, {longitude}.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/openuv/translations/de.json b/homeassistant/components/openuv/translations/de.json index 94d8b49b7d5e34..9e5c9d42b62660 100644 --- a/homeassistant/components/openuv/translations/de.json +++ b/homeassistant/components/openuv/translations/de.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Standort ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Bitte gib den API-Schl\u00fcssel f\u00fcr {latitude}, {longitude} erneut ein.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", diff --git a/homeassistant/components/openuv/translations/id.json b/homeassistant/components/openuv/translations/id.json index 6ad5ee1f8d9426..f7ef8c51b696e2 100644 --- a/homeassistant/components/openuv/translations/id.json +++ b/homeassistant/components/openuv/translations/id.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" + "already_configured": "Lokasi sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_api_key": "Kunci API tidak valid" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Masukkan kembali kunci API untuk {latitude}, {longitude}.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "api_key": "Kunci API", diff --git a/homeassistant/components/openuv/translations/no.json b/homeassistant/components/openuv/translations/no.json index 1fcba27dc9f9a5..948b1e833696af 100644 --- a/homeassistant/components/openuv/translations/no.json +++ b/homeassistant/components/openuv/translations/no.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Plasseringen er allerede konfigurert" + "already_configured": "Plasseringen er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "invalid_api_key": "Ugyldig API-n\u00f8kkel" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Vennligst skriv inn API-n\u00f8kkelen for {latitude} , {longitude} .", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/openuv/translations/pl.json b/homeassistant/components/openuv/translations/pl.json index 6578e6fcf84017..530b09aaddca17 100644 --- a/homeassistant/components/openuv/translations/pl.json +++ b/homeassistant/components/openuv/translations/pl.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "invalid_api_key": "Nieprawid\u0142owy klucz API" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "description": "Wprowad\u017a ponownie klucz API dla {latitude}, {longitude}.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/openuv/translations/zh-Hant.json b/homeassistant/components/openuv/translations/zh-Hant.json index eaeeec74e3ce23..d8b06ae7a2adec 100644 --- a/homeassistant/components/openuv/translations/zh-Hant.json +++ b/homeassistant/components/openuv/translations/zh-Hant.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_api_key": "API \u91d1\u9470\u7121\u6548" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 {latitude}\u3001{longitude} API \u5bc6\u9470\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/ovo_energy/translations/bg.json b/homeassistant/components/ovo_energy/translations/bg.json index b0c9e8a77cc5a4..7207f59fef20f3 100644 --- a/homeassistant/components/ovo_energy/translations/bg.json +++ b/homeassistant/components/ovo_energy/translations/bg.json @@ -11,7 +11,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" }, - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/ring/translations/bg.json b/homeassistant/components/ring/translations/bg.json index dfe9fcc384e851..1b9e9ad53d6ebf 100644 --- a/homeassistant/components/ring/translations/bg.json +++ b/homeassistant/components/ring/translations/bg.json @@ -12,7 +12,7 @@ "data": { "2fa": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u0435\u043d \u043a\u043e\u0434" }, - "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/sensor/translations/bg.json b/homeassistant/components/sensor/translations/bg.json index f4ea74ca57e377..c72139b3552419 100644 --- a/homeassistant/components/sensor/translations/bg.json +++ b/homeassistant/components/sensor/translations/bg.json @@ -12,7 +12,7 @@ "is_value": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}" }, "trigger_type": { - "battery_level": "{entity_name} \u043d\u0438\u0432\u043e\u0442\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u044f", + "battery_level": "{entity_name} \u043f\u0440\u0438 \u043f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u043d\u0438\u0432\u043e\u0442\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430", "humidity": "{entity_name} \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", "illuminance": "{entity_name} \u043e\u0441\u0432\u0435\u0442\u0435\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", "power": "\u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index 1cdcd4e5d865a3..36889af941c5ff 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.", "unsupported_firmware": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0444\u044a\u0440\u043c\u0443\u0435\u0440\u0430." }, "error": { diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index 86e02b8cc07ead..00813f3438016a 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -11,7 +11,7 @@ }, "step": { "two_factor": { - "description": "\u0418\u0437\u0438\u0441\u043a\u0432\u0430 \u0441\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "description": "\u0418\u0437\u0438\u0441\u043a\u0432\u0430 \u0441\u0435 \u0434\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { "data": { diff --git a/homeassistant/components/totalconnect/translations/bg.json b/homeassistant/components/totalconnect/translations/bg.json index 70789b8fe099c8..8213742fc09553 100644 --- a/homeassistant/components/totalconnect/translations/bg.json +++ b/homeassistant/components/totalconnect/translations/bg.json @@ -5,7 +5,7 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "locations": { diff --git a/homeassistant/components/trafikverket_weatherstation/translations/bg.json b/homeassistant/components/trafikverket_weatherstation/translations/bg.json index c4f6c0a2f55991..bc91923312fd97 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/bg.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/bg.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "user": { diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 7532b2d9e60c15..39e57c2ed49820 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -43,8 +43,8 @@ }, "issues": { "ea_warning": { - "description": "Est\u00e0s utilitzant la {version} d'UniFi Protect. Les versions d'acc\u00e9s anticipat no s\u00f3n compatibles amb Home Assistant i poden fer que la teva integraci\u00f3 d'UniFi Protect s'espatlli o no funcioni correctament.", - "title": "{version} \u00e9s una versi\u00f3 d'acc\u00e9s anticipat d'UniFi Protect" + "description": "Est\u00e0s utilitzant UniFi Protect v{version} que \u00e9s una versi\u00f3 d'acc\u00e9s anticipat. Les versions d'acc\u00e9s anticipat no s\u00f3n compatibles amb Home Assistant i poden fer que la teva integraci\u00f3 d'UniFi Protect s'espatlli o no funcioni correctament.", + "title": "UniFi Protect v{version} \u00e9s una versi\u00f3 d'acc\u00e9s anticipat" } }, "options": { diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 1c4c44c9dffae4..8130df0e83765b 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -2,10 +2,10 @@ "config": { "abort": { "not_zha_device": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 zha \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 ZHA." + "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { - "cannot_connect": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 ZHA \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "flow_title": "{name}", "step": { @@ -100,18 +100,27 @@ "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0438\u0437\u0442\u044a\u0440\u0432\u0430\u043d\u043e", "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043e\u0431\u044a\u0440\u043d\u0430\u0442\u043e \"{subtype}\"", "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \"{subtype}\"", + "device_offline": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043e\u0444\u043b\u0430\u0439\u043d", "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \"{subtype}\"", "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0433\u043e \u0435 \u043f\u043b\u044a\u0437\u043d\u0430\u0442\u043e \"{subtype}\"", "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0435\u043d\u043e", - "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", - "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", - "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", - "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", - "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e" + "remote_button_alt_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435 (\u0430\u043b\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0435\u043d \u0440\u0435\u0436\u0438\u043c)", + "remote_button_double_press": "\"{subtype}\" \u043f\u0440\u0438 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_long_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quintuple_press": "\"{subtype}\" \u043f\u0440\u0438 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_press": "\"{subtype}\" \u043f\u0440\u0438 \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_short_release": "\"{subtype}\" \u043f\u0440\u0438 \u043e\u0442\u043f\u0443\u0441\u043a\u0430\u043d\u0435", + "remote_button_triple_press": "\"{subtype}\" \u043f\u0440\u0438 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435" } }, "options": { @@ -119,9 +128,6 @@ "not_zha_device": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0435 zha \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, - "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" - }, "flow_title": "{name}", "step": { "choose_formation_strategy": { @@ -149,7 +155,7 @@ "data": { "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" }, - "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0441\u0432\u043e\u044f \u0442\u0438\u043f Zigbee \u0440\u0430\u0434\u0438\u043e", + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0442\u0438\u043f\u0430 \u043d\u0430 \u0432\u0430\u0448\u0435\u0442\u043e Zigbee \u0440\u0430\u0434\u0438\u043e", "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0438\u043e" }, "manual_port_config": { From f34de5072a53fe9b0d36dfe75fa697f4aa263376 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 9 Nov 2022 19:34:31 -0800 Subject: [PATCH 356/394] Bump gcal_sync to 4.0.1 to fix Google Calendar config flow (#81873) Bump gcal_sync to 4.0.1 This reverts test chagnes from PR #81562 that were actually incorrect given the calendar "get" API returns less information that the "CalendarList" api. --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/google/test_config_flow.py | 2 +- tests/components/google/test_init.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 9fc265fa287d0f..2bc84827cd69e3 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.0.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==4.0.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 2fa5e7d4f8bcc0..f789ab61f0398b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.0 +gcal-sync==4.0.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07a299c70b83d4..0f70d132de0a88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -554,7 +554,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==4.0.0 +gcal-sync==4.0.1 # homeassistant.components.geocaching geocachingapi==0.2.1 diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index bce3f4855c7a86..d8ddd6fe5886ae 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -104,7 +104,7 @@ async def primary_calendar( """Fixture to return the primary calendar.""" mock_calendar_get( "primary", - {"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"}, + {"id": primary_calendar_email, "summary": "Personal"}, exc=primary_calendar_error, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index a2f16f778fdab0..5e7696eec6881e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -768,7 +768,7 @@ async def test_assign_unique_id( mock_calendar_get( "primary", - {"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "reader"}, + {"id": EMAIL_ADDRESS, "summary": "Personal"}, ) mock_calendars_list({"items": [test_api_calendar]}) From 3089ca06c5c057a49ee84b910b66b5ad4499314b Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 10 Nov 2022 18:16:37 +1100 Subject: [PATCH 357/394] Add integration_type to qld_bushfire (#81895) define integration type --- homeassistant/components/qld_bushfire/manifest.json | 3 ++- homeassistant/generated/integrations.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index 366bbdc347983f..94e94dcb6ee98c 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -5,5 +5,6 @@ "requirements": ["georss_qld_bushfire_alert_client==0.5"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["georss_qld_bushfire_alert_client"] + "loggers": ["georss_qld_bushfire_alert_client"], + "integration_type": "service" } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ed1aa0a3647e87..aa6221a114003e 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4159,7 +4159,7 @@ }, "qld_bushfire": { "name": "Queensland Bushfire Alert", - "integration_type": "hub", + "integration_type": "service", "config_flow": false, "iot_class": "cloud_polling" }, From 0bd04068de2b9aebce26216b73a0c03584dfa48e Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 10 Nov 2022 09:40:22 +0100 Subject: [PATCH 358/394] Omit unit of measurement and device class equal to None (#81880) Omit unit of measurement and dev class none --- homeassistant/components/bthome/sensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index e7757c2e8723fe..61f7603039e7ba 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -177,13 +177,11 @@ # Used for count sensor (BTHomeSensorDeviceClass.COUNT, None): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.COUNT}", - device_class=None, state_class=SensorStateClass.MEASUREMENT, ), # Used for rotation sensor (BTHomeSensorDeviceClass.ROTATION, Units.DEGREE): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.ROTATION}_{Units.DEGREE}", - device_class=None, native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, ), @@ -234,8 +232,6 @@ # Used for UV index sensor (BTHomeSensorDeviceClass.UV_INDEX, None,): SensorEntityDescription( key=f"{BTHomeSensorDeviceClass.UV_INDEX}", - device_class=None, - native_unit_of_measurement=None, state_class=SensorStateClass.MEASUREMENT, ), } From 9ded2325223de3918e3f69aab8732487323b2214 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 10 Nov 2022 10:09:52 +0100 Subject: [PATCH 359/394] Move zwave_js addon manager to hassio integration (#81354) --- homeassistant/components/hassio/__init__.py | 218 +--- .../components/hassio/addon_manager.py | 373 ++++++ homeassistant/components/hassio/handler.py | 202 +++ homeassistant/components/zwave_js/__init__.py | 3 +- homeassistant/components/zwave_js/addon.py | 358 +----- .../components/zwave_js/config_flow.py | 11 +- tests/components/hassio/test_addon_manager.py | 1128 +++++++++++++++++ tests/components/zwave_js/conftest.py | 20 +- tests/components/zwave_js/test_addon.py | 30 - tests/components/zwave_js/test_config_flow.py | 2 +- 10 files changed, 1747 insertions(+), 598 deletions(-) create mode 100644 homeassistant/components/hassio/addon_manager.py create mode 100644 tests/components/hassio/test_addon_manager.py delete mode 100644 tests/components/zwave_js/test_addon.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index c811b35812e7ea..598871f57d5ae2 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -47,6 +47,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow +from .addon_manager import AddonError, AddonInfo, AddonManager, AddonState # noqa: F401 from .addon_panel import async_setup_addon_panel from .auth import async_setup_auth_view from .const import ( @@ -55,7 +56,6 @@ ATTR_AUTO_UPDATE, ATTR_CHANGELOG, ATTR_COMPRESSED, - ATTR_DISCOVERY, ATTR_FOLDERS, ATTR_HOMEASSISTANT, ATTR_INPUT, @@ -74,7 +74,25 @@ SupervisorEntityModel, ) from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F401 -from .handler import HassIO, HassioAPIError, api_data +from .handler import ( # noqa: F401 + HassIO, + HassioAPIError, + async_create_backup, + async_get_addon_discovery_info, + async_get_addon_info, + async_get_addon_store_info, + async_install_addon, + async_restart_addon, + async_set_addon_options, + async_start_addon, + async_stop_addon, + async_uninstall_addon, + async_update_addon, + async_update_core, + async_update_diagnostics, + async_update_os, + async_update_supervisor, +) from .http import HassIOView from .ingress import async_setup_ingress_view from .repairs import SupervisorRepairs @@ -221,202 +239,6 @@ class APIEndpointSettings(NamedTuple): } -@bind_hass -async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: - """Return add-on info. - - The add-on must be installed. - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - return await hassio.get_addon_info(slug) - - -@api_data -async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: - """Return add-on store info. - - The caller of the function should handle HassioAPIError. - """ - hassio: HassIO = hass.data[DOMAIN] - command = f"/store/addons/{slug}" - return await hassio.send_command(command, method="get") - - -@bind_hass -async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: - """Update Supervisor diagnostics toggle. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - return await hassio.update_diagnostics(diagnostics) - - -@bind_hass -@api_data -async def async_install_addon(hass: HomeAssistant, slug: str) -> dict: - """Install add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/install" - return await hassio.send_command(command, timeout=None) - - -@bind_hass -@api_data -async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: - """Uninstall add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/uninstall" - return await hassio.send_command(command, timeout=60) - - -@bind_hass -@api_data -async def async_update_addon( - hass: HomeAssistant, - slug: str, - backup: bool = False, -) -> dict: - """Update add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/update" - return await hassio.send_command( - command, - payload={"backup": backup}, - timeout=None, - ) - - -@bind_hass -@api_data -async def async_start_addon(hass: HomeAssistant, slug: str) -> dict: - """Start add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/start" - return await hassio.send_command(command, timeout=60) - - -@bind_hass -@api_data -async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict: - """Restart add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/restart" - return await hassio.send_command(command, timeout=None) - - -@bind_hass -@api_data -async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict: - """Stop add-on. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/stop" - return await hassio.send_command(command, timeout=60) - - -@bind_hass -@api_data -async def async_set_addon_options( - hass: HomeAssistant, slug: str, options: dict -) -> dict: - """Set add-on options. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = f"/addons/{slug}/options" - return await hassio.send_command(command, payload=options) - - -@bind_hass -async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None: - """Return discovery data for an add-on.""" - hassio = hass.data[DOMAIN] - data = await hassio.retrieve_discovery_messages() - discovered_addons = data[ATTR_DISCOVERY] - return next((addon for addon in discovered_addons if addon["addon"] == slug), None) - - -@bind_hass -@api_data -async def async_create_backup( - hass: HomeAssistant, payload: dict, partial: bool = False -) -> dict: - """Create a full or partial backup. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - backup_type = "partial" if partial else "full" - command = f"/backups/new/{backup_type}" - return await hassio.send_command(command, payload=payload, timeout=None) - - -@bind_hass -@api_data -async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict: - """Update Home Assistant Operating System. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = "/os/update" - return await hassio.send_command( - command, - payload={"version": version}, - timeout=None, - ) - - -@bind_hass -@api_data -async def async_update_supervisor(hass: HomeAssistant) -> dict: - """Update Home Assistant Supervisor. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = "/supervisor/update" - return await hassio.send_command(command, timeout=None) - - -@bind_hass -@api_data -async def async_update_core( - hass: HomeAssistant, version: str | None = None, backup: bool = False -) -> dict: - """Update Home Assistant Core. - - The caller of the function should handle HassioAPIError. - """ - hassio = hass.data[DOMAIN] - command = "/core/update" - return await hassio.send_command( - command, - payload={"version": version, "backup": backup}, - timeout=None, - ) - - @callback @bind_hass def get_info(hass): diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py new file mode 100644 index 00000000000000..ff3e903601823d --- /dev/null +++ b/homeassistant/components/hassio/addon_manager.py @@ -0,0 +1,373 @@ +"""Provide add-on management.""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable, Coroutine +from dataclasses import dataclass +from enum import Enum +from functools import partial, wraps +import logging +from typing import Any, TypeVar + +from typing_extensions import Concatenate, ParamSpec + +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .handler import ( + HassioAPIError, + async_create_backup, + async_get_addon_discovery_info, + async_get_addon_info, + async_get_addon_store_info, + async_install_addon, + async_restart_addon, + async_set_addon_options, + async_start_addon, + async_stop_addon, + async_uninstall_addon, + async_update_addon, +) + +_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager") +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def api_error( + error_message: str, +) -> Callable[ + [Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]], + Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]], +]: + """Handle HassioAPIError and raise a specific AddonError.""" + + def handle_hassio_api_error( + func: Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]] + ) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]: + """Handle a HassioAPIError.""" + + @wraps(func) + async def wrapper( + self: _AddonManagerT, *args: _P.args, **kwargs: _P.kwargs + ) -> _R: + """Wrap an add-on manager method.""" + try: + return_value = await func(self, *args, **kwargs) + except HassioAPIError as err: + raise AddonError( + f"{error_message.format(addon_name=self.addon_name)}: {err}" + ) from err + + return return_value + + return wrapper + + return handle_hassio_api_error + + +@dataclass +class AddonInfo: + """Represent the current add-on info state.""" + + options: dict[str, Any] + state: AddonState + update_available: bool + version: str | None + + +class AddonState(Enum): + """Represent the current state of the add-on.""" + + NOT_INSTALLED = "not_installed" + INSTALLING = "installing" + UPDATING = "updating" + NOT_RUNNING = "not_running" + RUNNING = "running" + + +class AddonManager: + """Manage the add-on. + + Methods may raise AddonError. + Only one instance of this class may exist per add-on + to keep track of running add-on tasks. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + addon_name: str, + addon_slug: str, + ) -> None: + """Set up the add-on manager.""" + self.addon_name = addon_name + self.addon_slug = addon_slug + self._hass = hass + self._logger = logger + self._install_task: asyncio.Task | None = None + self._restart_task: asyncio.Task | None = None + self._start_task: asyncio.Task | None = None + self._update_task: asyncio.Task | None = None + + def task_in_progress(self) -> bool: + """Return True if any of the add-on tasks are in progress.""" + return any( + task and not task.done() + for task in ( + self._restart_task, + self._install_task, + self._start_task, + self._update_task, + ) + ) + + @api_error("Failed to get the {addon_name} add-on discovery info") + async def async_get_addon_discovery_info(self) -> dict: + """Return add-on discovery info.""" + discovery_info = await async_get_addon_discovery_info( + self._hass, self.addon_slug + ) + + if not discovery_info: + raise AddonError(f"Failed to get {self.addon_name} add-on discovery info") + + discovery_info_config: dict = discovery_info["config"] + return discovery_info_config + + @api_error("Failed to get the {addon_name} add-on info") + async def async_get_addon_info(self) -> AddonInfo: + """Return and cache manager add-on info.""" + addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug) + self._logger.debug("Add-on store info: %s", addon_store_info) + if not addon_store_info["installed"]: + return AddonInfo( + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + addon_info = await async_get_addon_info(self._hass, self.addon_slug) + addon_state = self.async_get_addon_state(addon_info) + return AddonInfo( + options=addon_info["options"], + state=addon_state, + update_available=addon_info["update_available"], + version=addon_info["version"], + ) + + @callback + def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: + """Return the current state of the managed add-on.""" + addon_state = AddonState.NOT_RUNNING + + if addon_info["state"] == "started": + addon_state = AddonState.RUNNING + if self._install_task and not self._install_task.done(): + addon_state = AddonState.INSTALLING + if self._update_task and not self._update_task.done(): + addon_state = AddonState.UPDATING + + return addon_state + + @api_error("Failed to set the {addon_name} add-on options") + async def async_set_addon_options(self, config: dict) -> None: + """Set manager add-on options.""" + options = {"options": config} + await async_set_addon_options(self._hass, self.addon_slug, options) + + @api_error("Failed to install the {addon_name} add-on") + async def async_install_addon(self) -> None: + """Install the managed add-on.""" + await async_install_addon(self._hass, self.addon_slug) + + @api_error("Failed to uninstall the {addon_name} add-on") + async def async_uninstall_addon(self) -> None: + """Uninstall the managed add-on.""" + await async_uninstall_addon(self._hass, self.addon_slug) + + @api_error("Failed to update the {addon_name} add-on") + async def async_update_addon(self) -> None: + """Update the managed add-on if needed.""" + addon_info = await self.async_get_addon_info() + + if addon_info.state is AddonState.NOT_INSTALLED: + raise AddonError(f"{self.addon_name} add-on is not installed") + + if not addon_info.update_available: + return + + await self.async_create_backup() + await async_update_addon(self._hass, self.addon_slug) + + @api_error("Failed to start the {addon_name} add-on") + async def async_start_addon(self) -> None: + """Start the managed add-on.""" + await async_start_addon(self._hass, self.addon_slug) + + @api_error("Failed to restart the {addon_name} add-on") + async def async_restart_addon(self) -> None: + """Restart the managed add-on.""" + await async_restart_addon(self._hass, self.addon_slug) + + @api_error("Failed to stop the {addon_name} add-on") + async def async_stop_addon(self) -> None: + """Stop the managed add-on.""" + await async_stop_addon(self._hass, self.addon_slug) + + @api_error("Failed to create a backup of the {addon_name} add-on") + async def async_create_backup(self) -> None: + """Create a partial backup of the managed add-on.""" + addon_info = await self.async_get_addon_info() + name = f"addon_{self.addon_slug}_{addon_info.version}" + + self._logger.debug("Creating backup: %s", name) + await async_create_backup( + self._hass, + {"name": name, "addons": [self.addon_slug]}, + partial=True, + ) + + async def async_configure_addon( + self, + addon_config: dict[str, Any], + ) -> None: + """Configure the manager add-on, if needed.""" + addon_info = await self.async_get_addon_info() + + if addon_info.state is AddonState.NOT_INSTALLED: + raise AddonError(f"{self.addon_name} add-on is not installed") + + if addon_config != addon_info.options: + await self.async_set_addon_options(addon_config) + + @callback + def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that installs the managed add-on. + + Only schedule a new install task if the there's no running task. + """ + if not self._install_task or self._install_task.done(): + self._logger.info( + "%s add-on is not installed. Installing add-on", self.addon_name + ) + self._install_task = self._async_schedule_addon_operation( + self.async_install_addon, catch_error=catch_error + ) + return self._install_task + + @callback + def async_schedule_install_setup_addon( + self, + addon_config: dict[str, Any], + catch_error: bool = False, + ) -> asyncio.Task: + """Schedule a task that installs and sets up the managed add-on. + + Only schedule a new install task if the there's no running task. + """ + if not self._install_task or self._install_task.done(): + self._logger.info( + "%s add-on is not installed. Installing add-on", self.addon_name + ) + self._install_task = self._async_schedule_addon_operation( + self.async_install_addon, + partial( + self.async_configure_addon, + addon_config, + ), + self.async_start_addon, + catch_error=catch_error, + ) + return self._install_task + + @callback + def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that updates and sets up the managed add-on. + + Only schedule a new update task if the there's no running task. + """ + if not self._update_task or self._update_task.done(): + self._logger.info("Trying to update the %s add-on", self.addon_name) + self._update_task = self._async_schedule_addon_operation( + self.async_update_addon, + catch_error=catch_error, + ) + return self._update_task + + @callback + def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that starts the managed add-on. + + Only schedule a new start task if the there's no running task. + """ + if not self._start_task or self._start_task.done(): + self._logger.info( + "%s add-on is not running. Starting add-on", self.addon_name + ) + self._start_task = self._async_schedule_addon_operation( + self.async_start_addon, catch_error=catch_error + ) + return self._start_task + + @callback + def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task: + """Schedule a task that restarts the managed add-on. + + Only schedule a new restart task if the there's no running task. + """ + if not self._restart_task or self._restart_task.done(): + self._logger.info("Restarting %s add-on", self.addon_name) + self._restart_task = self._async_schedule_addon_operation( + self.async_restart_addon, catch_error=catch_error + ) + return self._restart_task + + @callback + def async_schedule_setup_addon( + self, + addon_config: dict[str, Any], + catch_error: bool = False, + ) -> asyncio.Task: + """Schedule a task that configures and starts the managed add-on. + + Only schedule a new setup task if there's no running task. + """ + if not self._start_task or self._start_task.done(): + self._logger.info( + "%s add-on is not running. Starting add-on", self.addon_name + ) + self._start_task = self._async_schedule_addon_operation( + partial( + self.async_configure_addon, + addon_config, + ), + self.async_start_addon, + catch_error=catch_error, + ) + return self._start_task + + @callback + def _async_schedule_addon_operation( + self, *funcs: Callable, catch_error: bool = False + ) -> asyncio.Task: + """Schedule an add-on task.""" + + async def addon_operation() -> None: + """Do the add-on operation and catch AddonError.""" + for func in funcs: + try: + await func() + except AddonError as err: + if not catch_error: + raise + self._logger.error(err) + break + + return self._hass.async_create_task(addon_operation()) + + +class AddonError(HomeAssistantError): + """Represent an error with the managed add-on.""" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index ee16bdf815869b..4f300ef16dbfe7 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -1,4 +1,6 @@ """Handler for Hass.io.""" +from __future__ import annotations + import asyncio from http import HTTPStatus import logging @@ -12,6 +14,10 @@ CONF_SSL_CERTIFICATE, ) from homeassistant.const import SERVER_PORT +from homeassistant.core import HomeAssistant +from homeassistant.loader import bind_hass + +from .const import ATTR_DISCOVERY, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -47,6 +53,202 @@ async def _wrapper(*argv, **kwargs): return _wrapper +@bind_hass +async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: + """Return add-on info. + + The add-on must be installed. + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + return await hassio.get_addon_info(slug) + + +@api_data +async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: + """Return add-on store info. + + The caller of the function should handle HassioAPIError. + """ + hassio: HassIO = hass.data[DOMAIN] + command = f"/store/addons/{slug}" + return await hassio.send_command(command, method="get") + + +@bind_hass +async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: + """Update Supervisor diagnostics toggle. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + return await hassio.update_diagnostics(diagnostics) + + +@bind_hass +@api_data +async def async_install_addon(hass: HomeAssistant, slug: str) -> dict: + """Install add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/install" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_uninstall_addon(hass: HomeAssistant, slug: str) -> dict: + """Uninstall add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/uninstall" + return await hassio.send_command(command, timeout=60) + + +@bind_hass +@api_data +async def async_update_addon( + hass: HomeAssistant, + slug: str, + backup: bool = False, +) -> dict: + """Update add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/update" + return await hassio.send_command( + command, + payload={"backup": backup}, + timeout=None, + ) + + +@bind_hass +@api_data +async def async_start_addon(hass: HomeAssistant, slug: str) -> dict: + """Start add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/start" + return await hassio.send_command(command, timeout=60) + + +@bind_hass +@api_data +async def async_restart_addon(hass: HomeAssistant, slug: str) -> dict: + """Restart add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/restart" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_stop_addon(hass: HomeAssistant, slug: str) -> dict: + """Stop add-on. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/stop" + return await hassio.send_command(command, timeout=60) + + +@bind_hass +@api_data +async def async_set_addon_options( + hass: HomeAssistant, slug: str, options: dict +) -> dict: + """Set add-on options. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = f"/addons/{slug}/options" + return await hassio.send_command(command, payload=options) + + +@bind_hass +async def async_get_addon_discovery_info(hass: HomeAssistant, slug: str) -> dict | None: + """Return discovery data for an add-on.""" + hassio = hass.data[DOMAIN] + data = await hassio.retrieve_discovery_messages() + discovered_addons = data[ATTR_DISCOVERY] + return next((addon for addon in discovered_addons if addon["addon"] == slug), None) + + +@bind_hass +@api_data +async def async_create_backup( + hass: HomeAssistant, payload: dict, partial: bool = False +) -> dict: + """Create a full or partial backup. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + backup_type = "partial" if partial else "full" + command = f"/backups/new/{backup_type}" + return await hassio.send_command(command, payload=payload, timeout=None) + + +@bind_hass +@api_data +async def async_update_os(hass: HomeAssistant, version: str | None = None) -> dict: + """Update Home Assistant Operating System. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/os/update" + return await hassio.send_command( + command, + payload={"version": version}, + timeout=None, + ) + + +@bind_hass +@api_data +async def async_update_supervisor(hass: HomeAssistant) -> dict: + """Update Home Assistant Supervisor. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/supervisor/update" + return await hassio.send_command(command, timeout=None) + + +@bind_hass +@api_data +async def async_update_core( + hass: HomeAssistant, version: str | None = None, backup: bool = False +) -> dict: + """Update Home Assistant Core. + + The caller of the function should handle HassioAPIError. + """ + hassio = hass.data[DOMAIN] + command = "/core/update" + return await hassio.send_command( + command, + payload={"version": version, "backup": backup}, + timeout=None, + ) + + class HassIO: """Small API wrapper for Hass.io.""" diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index cab07f4287f9cb..c492cd8618f4b8 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -20,6 +20,7 @@ ) from zwave_js_server.model.value import Value, ValueNotification +from homeassistant.components.hassio import AddonError, AddonManager, AddonState from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_DEVICE_ID, @@ -41,7 +42,7 @@ ) from homeassistant.helpers.typing import UNDEFINED, ConfigType -from .addon import AddonError, AddonManager, AddonState, get_addon_manager +from .addon import get_addon_manager from .api import async_register_api from .const import ( ATTR_ACKNOWLEDGED_FRAMES, diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 3e27235ef84823..f9adf9f19fbb4f 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -1,39 +1,12 @@ """Provide add-on management.""" from __future__ import annotations -import asyncio -from collections.abc import Awaitable, Callable, Coroutine -from dataclasses import dataclass -from enum import Enum -from functools import partial, wraps -from typing import Any, TypeVar - -from typing_extensions import Concatenate, ParamSpec - -from homeassistant.components.hassio import ( - async_create_backup, - async_get_addon_discovery_info, - async_get_addon_info, - async_get_addon_store_info, - async_install_addon, - async_restart_addon, - async_set_addon_options, - async_start_addon, - async_stop_addon, - async_uninstall_addon, - async_update_addon, -) -from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.hassio import AddonManager from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.singleton import singleton from .const import ADDON_SLUG, DOMAIN, LOGGER -_AddonManagerT = TypeVar("_AddonManagerT", bound="AddonManager") -_R = TypeVar("_R") -_P = ParamSpec("_P") - DATA_ADDON_MANAGER = f"{DOMAIN}_addon_manager" @@ -41,331 +14,4 @@ @callback def get_addon_manager(hass: HomeAssistant) -> AddonManager: """Get the add-on manager.""" - return AddonManager(hass, "Z-Wave JS", ADDON_SLUG) - - -def api_error( - error_message: str, -) -> Callable[ - [Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]]], - Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]], -]: - """Handle HassioAPIError and raise a specific AddonError.""" - - def handle_hassio_api_error( - func: Callable[Concatenate[_AddonManagerT, _P], Awaitable[_R]] - ) -> Callable[Concatenate[_AddonManagerT, _P], Coroutine[Any, Any, _R]]: - """Handle a HassioAPIError.""" - - @wraps(func) - async def wrapper( - self: _AddonManagerT, *args: _P.args, **kwargs: _P.kwargs - ) -> _R: - """Wrap an add-on manager method.""" - try: - return_value = await func(self, *args, **kwargs) - except HassioAPIError as err: - raise AddonError( - f"{error_message.format(addon_name=self.addon_name)}: {err}" - ) from err - - return return_value - - return wrapper - - return handle_hassio_api_error - - -@dataclass -class AddonInfo: - """Represent the current add-on info state.""" - - options: dict[str, Any] - state: AddonState - update_available: bool - version: str | None - - -class AddonState(Enum): - """Represent the current state of the add-on.""" - - NOT_INSTALLED = "not_installed" - INSTALLING = "installing" - UPDATING = "updating" - NOT_RUNNING = "not_running" - RUNNING = "running" - - -class AddonManager: - """Manage the add-on. - - Methods may raise AddonError. - Only one instance of this class may exist per add-on - to keep track of running add-on tasks. - """ - - def __init__(self, hass: HomeAssistant, addon_name: str, addon_slug: str) -> None: - """Set up the add-on manager.""" - self.addon_name = addon_name - self.addon_slug = addon_slug - self._hass = hass - self._install_task: asyncio.Task | None = None - self._restart_task: asyncio.Task | None = None - self._start_task: asyncio.Task | None = None - self._update_task: asyncio.Task | None = None - - def task_in_progress(self) -> bool: - """Return True if any of the add-on tasks are in progress.""" - return any( - task and not task.done() - for task in ( - self._install_task, - self._start_task, - self._update_task, - ) - ) - - @api_error("Failed to get {addon_name} add-on discovery info") - async def async_get_addon_discovery_info(self) -> dict: - """Return add-on discovery info.""" - discovery_info = await async_get_addon_discovery_info( - self._hass, self.addon_slug - ) - - if not discovery_info: - raise AddonError(f"Failed to get {self.addon_name} add-on discovery info") - - discovery_info_config: dict = discovery_info["config"] - return discovery_info_config - - @api_error("Failed to get the {addon_name} add-on info") - async def async_get_addon_info(self) -> AddonInfo: - """Return and cache manager add-on info.""" - addon_store_info = await async_get_addon_store_info(self._hass, self.addon_slug) - LOGGER.debug("Add-on store info: %s", addon_store_info) - if not addon_store_info["installed"]: - return AddonInfo( - options={}, - state=AddonState.NOT_INSTALLED, - update_available=False, - version=None, - ) - - addon_info = await async_get_addon_info(self._hass, self.addon_slug) - addon_state = self.async_get_addon_state(addon_info) - return AddonInfo( - options=addon_info["options"], - state=addon_state, - update_available=addon_info["update_available"], - version=addon_info["version"], - ) - - @callback - def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: - """Return the current state of the managed add-on.""" - addon_state = AddonState.NOT_RUNNING - - if addon_info["state"] == "started": - addon_state = AddonState.RUNNING - if self._install_task and not self._install_task.done(): - addon_state = AddonState.INSTALLING - if self._update_task and not self._update_task.done(): - addon_state = AddonState.UPDATING - - return addon_state - - @api_error("Failed to set the {addon_name} add-on options") - async def async_set_addon_options(self, config: dict) -> None: - """Set manager add-on options.""" - options = {"options": config} - await async_set_addon_options(self._hass, self.addon_slug, options) - - @api_error("Failed to install the {addon_name} add-on") - async def async_install_addon(self) -> None: - """Install the managed add-on.""" - await async_install_addon(self._hass, self.addon_slug) - - @callback - def async_schedule_install_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that installs the managed add-on. - - Only schedule a new install task if the there's no running task. - """ - if not self._install_task or self._install_task.done(): - LOGGER.info( - "%s add-on is not installed. Installing add-on", self.addon_name - ) - self._install_task = self._async_schedule_addon_operation( - self.async_install_addon, catch_error=catch_error - ) - return self._install_task - - @callback - def async_schedule_install_setup_addon( - self, - addon_config: dict[str, Any], - catch_error: bool = False, - ) -> asyncio.Task: - """Schedule a task that installs and sets up the managed add-on. - - Only schedule a new install task if the there's no running task. - """ - if not self._install_task or self._install_task.done(): - LOGGER.info( - "%s add-on is not installed. Installing add-on", self.addon_name - ) - self._install_task = self._async_schedule_addon_operation( - self.async_install_addon, - partial( - self.async_configure_addon, - addon_config, - ), - self.async_start_addon, - catch_error=catch_error, - ) - return self._install_task - - @api_error("Failed to uninstall the {addon_name} add-on") - async def async_uninstall_addon(self) -> None: - """Uninstall the managed add-on.""" - await async_uninstall_addon(self._hass, self.addon_slug) - - @api_error("Failed to update the {addon_name} add-on") - async def async_update_addon(self) -> None: - """Update the managed add-on if needed.""" - addon_info = await self.async_get_addon_info() - - if addon_info.state is AddonState.NOT_INSTALLED: - raise AddonError(f"{self.addon_name} add-on is not installed") - - if not addon_info.update_available: - return - - await self.async_create_backup() - await async_update_addon(self._hass, self.addon_slug) - - @callback - def async_schedule_update_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that updates and sets up the managed add-on. - - Only schedule a new update task if the there's no running task. - """ - if not self._update_task or self._update_task.done(): - LOGGER.info("Trying to update the %s add-on", self.addon_name) - self._update_task = self._async_schedule_addon_operation( - self.async_update_addon, - catch_error=catch_error, - ) - return self._update_task - - @api_error("Failed to start the {addon_name} add-on") - async def async_start_addon(self) -> None: - """Start the managed add-on.""" - await async_start_addon(self._hass, self.addon_slug) - - @api_error("Failed to restart the {addon_name} add-on") - async def async_restart_addon(self) -> None: - """Restart the managed add-on.""" - await async_restart_addon(self._hass, self.addon_slug) - - @callback - def async_schedule_start_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that starts the managed add-on. - - Only schedule a new start task if the there's no running task. - """ - if not self._start_task or self._start_task.done(): - LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name) - self._start_task = self._async_schedule_addon_operation( - self.async_start_addon, catch_error=catch_error - ) - return self._start_task - - @callback - def async_schedule_restart_addon(self, catch_error: bool = False) -> asyncio.Task: - """Schedule a task that restarts the managed add-on. - - Only schedule a new restart task if the there's no running task. - """ - if not self._restart_task or self._restart_task.done(): - LOGGER.info("Restarting %s add-on", self.addon_name) - self._restart_task = self._async_schedule_addon_operation( - self.async_restart_addon, catch_error=catch_error - ) - return self._restart_task - - @api_error("Failed to stop the {addon_name} add-on") - async def async_stop_addon(self) -> None: - """Stop the managed add-on.""" - await async_stop_addon(self._hass, self.addon_slug) - - async def async_configure_addon( - self, - addon_config: dict[str, Any], - ) -> None: - """Configure and start manager add-on.""" - addon_info = await self.async_get_addon_info() - - if addon_info.state is AddonState.NOT_INSTALLED: - raise AddonError(f"{self.addon_name} add-on is not installed") - - if addon_config != addon_info.options: - await self.async_set_addon_options(addon_config) - - @callback - def async_schedule_setup_addon( - self, - addon_config: dict[str, Any], - catch_error: bool = False, - ) -> asyncio.Task: - """Schedule a task that configures and starts the managed add-on. - - Only schedule a new setup task if there's no running task. - """ - if not self._start_task or self._start_task.done(): - LOGGER.info("%s add-on is not running. Starting add-on", self.addon_name) - self._start_task = self._async_schedule_addon_operation( - partial( - self.async_configure_addon, - addon_config, - ), - self.async_start_addon, - catch_error=catch_error, - ) - return self._start_task - - @api_error("Failed to create a backup of the {addon_name} add-on.") - async def async_create_backup(self) -> None: - """Create a partial backup of the managed add-on.""" - addon_info = await self.async_get_addon_info() - name = f"addon_{self.addon_slug}_{addon_info.version}" - - LOGGER.debug("Creating backup: %s", name) - await async_create_backup( - self._hass, - {"name": name, "addons": [self.addon_slug]}, - partial=True, - ) - - @callback - def _async_schedule_addon_operation( - self, *funcs: Callable, catch_error: bool = False - ) -> asyncio.Task: - """Schedule an add-on task.""" - - async def addon_operation() -> None: - """Do the add-on operation and catch AddonError.""" - for func in funcs: - try: - await func() - except AddonError as err: - if not catch_error: - raise - LOGGER.error(err) - break - - return self._hass.async_create_task(addon_operation()) - - -class AddonError(HomeAssistantError): - """Represent an error with the managed add-on.""" + return AddonManager(hass, LOGGER, "Z-Wave JS", ADDON_SLUG) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 0a084b3a309533..11fd3da0e75cb5 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -14,7 +14,14 @@ from homeassistant import config_entries, exceptions from homeassistant.components import usb -from homeassistant.components.hassio import HassioServiceInfo, is_hassio +from homeassistant.components.hassio import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, + HassioServiceInfo, + is_hassio, +) from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant, callback @@ -27,7 +34,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import disconnect_client -from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager +from .addon import get_addon_manager from .const import ( ADDON_SLUG, CONF_ADDON_DEVICE, diff --git a/tests/components/hassio/test_addon_manager.py b/tests/components/hassio/test_addon_manager.py new file mode 100644 index 00000000000000..ffeecd167e6f10 --- /dev/null +++ b/tests/components/hassio/test_addon_manager.py @@ -0,0 +1,1128 @@ +"""Test the addon manager.""" +from __future__ import annotations + +import asyncio +from collections.abc import Generator +import logging +from typing import Any +from unittest.mock import AsyncMock, call, patch + +import pytest + +from homeassistant.components.hassio.addon_manager import ( + AddonError, + AddonInfo, + AddonManager, + AddonState, +) +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.core import HomeAssistant + +LOGGER = logging.getLogger(__name__) + + +@pytest.fixture(name="addon_manager") +def addon_manager_fixture(hass: HomeAssistant) -> AddonManager: + """Return an AddonManager instance.""" + return AddonManager(hass, LOGGER, "Test", "test_addon") + + +@pytest.fixture(name="addon_not_installed") +def addon_not_installed_fixture( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> AsyncMock: + """Mock add-on not installed.""" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed( + addon_store_info: AsyncMock, addon_info: AsyncMock +) -> AsyncMock: + """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="get_addon_discovery_info") +def get_addon_discovery_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock get add-on discovery info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info" + ) as get_addon_discovery_info: + yield get_addon_discovery_info + + +@pytest.fixture(name="addon_store_info") +def addon_store_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock Supervisor add-on store info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info" + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + +@pytest.fixture(name="addon_info") +def addon_info_fixture() -> Generator[AsyncMock, None, None]: + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_get_addon_info", + ) as addon_info: + addon_info.return_value = { + "options": {}, + "state": None, + "update_available": False, + "version": None, + } + yield addon_info + + +@pytest.fixture(name="set_addon_options") +def set_addon_options_fixture() -> Generator[AsyncMock, None, None]: + """Mock set add-on options.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_set_addon_options" + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon") +def install_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock install add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_install_addon" + ) as install_addon: + yield install_addon + + +@pytest.fixture(name="uninstall_addon") +def uninstall_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon + + +@pytest.fixture(name="start_addon") +def start_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock start add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_start_addon" + ) as start_addon: + yield start_addon + + +@pytest.fixture(name="restart_addon") +def restart_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock restart add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_restart_addon" + ) as restart_addon: + yield restart_addon + + +@pytest.fixture(name="stop_addon") +def stop_addon_fixture() -> Generator[AsyncMock, None, None]: + """Mock stop add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_stop_addon" + ) as stop_addon: + yield stop_addon + + +@pytest.fixture(name="create_backup") +def create_backup_fixture() -> Generator[AsyncMock, None, None]: + """Mock create backup.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_create_backup" + ) as create_backup: + yield create_backup + + +@pytest.fixture(name="update_addon") +def mock_update_addon() -> Generator[AsyncMock, None, None]: + """Mock update add-on.""" + with patch( + "homeassistant.components.hassio.addon_manager.async_update_addon" + ) as update_addon: + yield update_addon + + +async def test_not_installed_raises_exception( + addon_manager: AddonManager, + addon_not_installed: dict[str, Any], +) -> None: + """Test addon not installed raises exception.""" + addon_config = {"test_key": "test"} + + with pytest.raises(AddonError) as err: + await addon_manager.async_configure_addon(addon_config) + + assert str(err.value) == "Test add-on is not installed" + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Test add-on is not installed" + + +async def test_get_addon_discovery_info( + addon_manager: AddonManager, get_addon_discovery_info: AsyncMock +) -> None: + """Test get addon discovery info.""" + get_addon_discovery_info.return_value = {"config": {"test_key": "test"}} + + assert await addon_manager.async_get_addon_discovery_info() == {"test_key": "test"} + + assert get_addon_discovery_info.call_count == 1 + + +async def test_missing_addon_discovery_info( + addon_manager: AddonManager, get_addon_discovery_info: AsyncMock +) -> None: + """Test missing addon discovery info.""" + get_addon_discovery_info.return_value = None + + with pytest.raises(AddonError): + await addon_manager.async_get_addon_discovery_info() + + assert get_addon_discovery_info.call_count == 1 + + +async def test_get_addon_discovery_info_error( + addon_manager: AddonManager, get_addon_discovery_info: AsyncMock +) -> None: + """Test get addon discovery info raises error.""" + get_addon_discovery_info.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + assert await addon_manager.async_get_addon_discovery_info() + + assert str(err.value) == "Failed to get the Test add-on discovery info: Boom" + + assert get_addon_discovery_info.call_count == 1 + + +async def test_get_addon_info_not_installed( + addon_manager: AddonManager, addon_not_installed: AsyncMock +) -> None: + """Test get addon info when addon is not installed..""" + assert await addon_manager.async_get_addon_info() == AddonInfo( + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + +@pytest.mark.parametrize( + "addon_info_state, addon_state", + [("started", AddonState.RUNNING), ("stopped", AddonState.NOT_RUNNING)], +) +async def test_get_addon_info( + addon_manager: AddonManager, + addon_installed: AsyncMock, + addon_info_state: str, + addon_state: AddonState, +) -> None: + """Test get addon info when addon is installed.""" + addon_installed.return_value["state"] = addon_info_state + assert await addon_manager.async_get_addon_info() == AddonInfo( + options={}, + state=addon_state, + update_available=False, + version="1.0.0", + ) + + +@pytest.mark.parametrize( + "addon_info_error, addon_info_calls, addon_store_info_error, addon_store_info_calls", + [(HassioAPIError("Boom"), 1, None, 1), (None, 0, HassioAPIError("Boom"), 1)], +) +async def test_get_addon_info_error( + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_store_info: AsyncMock, + addon_installed: AsyncMock, + addon_info_error: Exception | None, + addon_info_calls: int, + addon_store_info_error: Exception | None, + addon_store_info_calls: int, +) -> None: + """Test get addon info raises error.""" + addon_info.side_effect = addon_info_error + addon_store_info.side_effect = addon_store_info_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_get_addon_info() + + assert str(err.value) == "Failed to get the Test add-on info: Boom" + + assert addon_info.call_count == addon_info_calls + assert addon_store_info.call_count == addon_store_info_calls + + +async def test_set_addon_options( + hass: HomeAssistant, addon_manager: AddonManager, set_addon_options: AsyncMock +) -> None: + """Test set addon options.""" + await addon_manager.async_set_addon_options({"test_key": "test"}) + + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + hass, "test_addon", {"options": {"test_key": "test"}} + ) + + +async def test_set_addon_options_error( + hass: HomeAssistant, addon_manager: AddonManager, set_addon_options: AsyncMock +) -> None: + """Test set addon options raises error.""" + set_addon_options.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_set_addon_options({"test_key": "test"}) + + assert str(err.value) == "Failed to set the Test add-on options: Boom" + + assert set_addon_options.call_count == 1 + assert set_addon_options.call_args == call( + hass, "test_addon", {"options": {"test_key": "test"}} + ) + + +async def test_install_addon( + addon_manager: AddonManager, install_addon: AsyncMock +) -> None: + """Test install addon.""" + await addon_manager.async_install_addon() + + assert install_addon.call_count == 1 + + +async def test_install_addon_error( + addon_manager: AddonManager, install_addon: AsyncMock +) -> None: + """Test install addon raises error.""" + install_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_install_addon() + + assert str(err.value) == "Failed to install the Test add-on: Boom" + + assert install_addon.call_count == 1 + + +async def test_schedule_install_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, +) -> None: + """Test schedule install addon.""" + install_task = addon_manager.async_schedule_install_addon() + + assert addon_manager.task_in_progress() is True + + assert await addon_manager.async_get_addon_info() == AddonInfo( + options={}, + state=AddonState.INSTALLING, + update_available=False, + version="1.0.0", + ) + + # Make sure that actually only one install task is running. + install_task_two = addon_manager.async_schedule_install_addon() + + await asyncio.gather(install_task, install_task_two) + + assert addon_manager.task_in_progress() is False + assert install_addon.call_count == 1 + + install_addon.reset_mock() + + # Test that another call can be made after the install is done. + await addon_manager.async_schedule_install_addon() + + assert install_addon.call_count == 1 + + +async def test_schedule_install_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, +) -> None: + """Test schedule install addon raises error.""" + install_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_install_addon() + + assert str(err.value) == "Failed to install the Test add-on: Boom" + + assert install_addon.call_count == 1 + + +async def test_schedule_install_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule install addon logs error.""" + install_addon.side_effect = HassioAPIError("Boom") + + await addon_manager.async_schedule_install_addon(catch_error=True) + + assert "Failed to install the Test add-on: Boom" in caplog.text + assert install_addon.call_count == 1 + + +async def test_uninstall_addon( + addon_manager: AddonManager, uninstall_addon: AsyncMock +) -> None: + """Test uninstall addon.""" + await addon_manager.async_uninstall_addon() + + assert uninstall_addon.call_count == 1 + + +async def test_uninstall_addon_error( + addon_manager: AddonManager, uninstall_addon: AsyncMock +) -> None: + """Test uninstall addon raises error.""" + uninstall_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_uninstall_addon() + + assert str(err.value) == "Failed to uninstall the Test add-on: Boom" + + assert uninstall_addon.call_count == 1 + + +async def test_start_addon(addon_manager: AddonManager, start_addon: AsyncMock) -> None: + """Test start addon.""" + await addon_manager.async_start_addon() + + assert start_addon.call_count == 1 + + +async def test_start_addon_error( + addon_manager: AddonManager, start_addon: AsyncMock +) -> None: + """Test start addon raises error.""" + start_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_start_addon() + + assert str(err.value) == "Failed to start the Test add-on: Boom" + + assert start_addon.call_count == 1 + + +async def test_schedule_start_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule start addon.""" + start_task = addon_manager.async_schedule_start_addon() + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one start task is running. + start_task_two = addon_manager.async_schedule_start_addon() + + await asyncio.gather(start_task, start_task_two) + + assert addon_manager.task_in_progress() is False + assert start_addon.call_count == 1 + + start_addon.reset_mock() + + # Test that another call can be made after the start is done. + await addon_manager.async_schedule_start_addon() + + assert start_addon.call_count == 1 + + +async def test_schedule_start_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule start addon raises error.""" + start_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_start_addon() + + assert str(err.value) == "Failed to start the Test add-on: Boom" + + assert start_addon.call_count == 1 + + +async def test_schedule_start_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + start_addon: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule start addon logs error.""" + start_addon.side_effect = HassioAPIError("Boom") + + await addon_manager.async_schedule_start_addon(catch_error=True) + + assert "Failed to start the Test add-on: Boom" in caplog.text + assert start_addon.call_count == 1 + + +async def test_restart_addon( + addon_manager: AddonManager, restart_addon: AsyncMock +) -> None: + """Test restart addon.""" + await addon_manager.async_restart_addon() + + assert restart_addon.call_count == 1 + + +async def test_restart_addon_error( + addon_manager: AddonManager, restart_addon: AsyncMock +) -> None: + """Test restart addon raises error.""" + restart_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_restart_addon() + + assert str(err.value) == "Failed to restart the Test add-on: Boom" + + assert restart_addon.call_count == 1 + + +async def test_schedule_restart_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + restart_addon: AsyncMock, +) -> None: + """Test schedule restart addon.""" + restart_task = addon_manager.async_schedule_restart_addon() + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one start task is running. + restart_task_two = addon_manager.async_schedule_restart_addon() + + await asyncio.gather(restart_task, restart_task_two) + + assert addon_manager.task_in_progress() is False + assert restart_addon.call_count == 1 + + restart_addon.reset_mock() + + # Test that another call can be made after the restart is done. + await addon_manager.async_schedule_restart_addon() + + assert restart_addon.call_count == 1 + + +async def test_schedule_restart_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + restart_addon: AsyncMock, +) -> None: + """Test schedule restart addon raises error.""" + restart_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_restart_addon() + + assert str(err.value) == "Failed to restart the Test add-on: Boom" + + assert restart_addon.call_count == 1 + + +async def test_schedule_restart_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + restart_addon: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule restart addon logs error.""" + restart_addon.side_effect = HassioAPIError("Boom") + + await addon_manager.async_schedule_restart_addon(catch_error=True) + + assert "Failed to restart the Test add-on: Boom" in caplog.text + assert restart_addon.call_count == 1 + + +async def test_stop_addon(addon_manager: AddonManager, stop_addon: AsyncMock) -> None: + """Test stop addon.""" + await addon_manager.async_stop_addon() + + assert stop_addon.call_count == 1 + + +async def test_stop_addon_error( + addon_manager: AddonManager, stop_addon: AsyncMock +) -> None: + """Test stop addon raises error.""" + stop_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_stop_addon() + + assert str(err.value) == "Failed to stop the Test add-on: Boom" + + assert stop_addon.call_count == 1 + + +async def test_update_addon( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test update addon.""" + addon_info.return_value["update_available"] = True + + await addon_manager.async_update_addon() + + assert addon_info.call_count == 2 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + assert update_addon.call_count == 1 + + +async def test_update_addon_no_update( + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test update addon without update available.""" + addon_info.return_value["update_available"] = False + + await addon_manager.async_update_addon() + + assert addon_info.call_count == 1 + assert create_backup.call_count == 0 + assert update_addon.call_count == 0 + + +async def test_update_addon_error( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test update addon raises error.""" + addon_info.return_value["update_available"] = True + update_addon.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_update_addon() + + assert str(err.value) == "Failed to update the Test add-on: Boom" + + assert addon_info.call_count == 2 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + assert update_addon.call_count == 1 + + +async def test_schedule_update_addon( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, +) -> None: + """Test schedule update addon.""" + addon_info.return_value["update_available"] = True + + update_task = addon_manager.async_schedule_update_addon() + + assert addon_manager.task_in_progress() is True + + assert await addon_manager.async_get_addon_info() == AddonInfo( + options={}, + state=AddonState.UPDATING, + update_available=True, + version="1.0.0", + ) + + # Make sure that actually only one update task is running. + update_task_two = addon_manager.async_schedule_update_addon() + + await asyncio.gather(update_task, update_task_two) + + assert addon_manager.task_in_progress() is False + assert addon_info.call_count == 3 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + assert update_addon.call_count == 1 + + update_addon.reset_mock() + + # Test that another call can be made after the update is done. + await addon_manager.async_schedule_update_addon() + + assert update_addon.call_count == 1 + + +@pytest.mark.parametrize( + ( + "create_backup_error, create_backup_calls, " + "update_addon_error, update_addon_calls, " + "error_message" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to create a backup of the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to update the Test add-on: Boom", + ), + ], +) +async def test_schedule_update_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, + create_backup_error: Exception | None, + create_backup_calls: int, + update_addon_error: Exception | None, + update_addon_calls: int, + error_message: str, +) -> None: + """Test schedule update addon raises error.""" + addon_installed.return_value["update_available"] = True + create_backup.side_effect = create_backup_error + update_addon.side_effect = update_addon_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_update_addon() + + assert str(err.value) == error_message + + assert create_backup.call_count == create_backup_calls + assert update_addon.call_count == update_addon_calls + + +@pytest.mark.parametrize( + ( + "create_backup_error, create_backup_calls, " + "update_addon_error, update_addon_calls, " + "error_log" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to create a backup of the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to update the Test add-on: Boom", + ), + ], +) +async def test_schedule_update_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + create_backup: AsyncMock, + update_addon: AsyncMock, + create_backup_error: Exception | None, + create_backup_calls: int, + update_addon_error: Exception | None, + update_addon_calls: int, + error_log: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule update addon logs error.""" + addon_installed.return_value["update_available"] = True + create_backup.side_effect = create_backup_error + update_addon.side_effect = update_addon_error + + await addon_manager.async_schedule_update_addon(catch_error=True) + + assert error_log in caplog.text + assert create_backup.call_count == create_backup_calls + assert update_addon.call_count == update_addon_calls + + +async def test_create_backup( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, +) -> None: + """Test creating a backup of the addon.""" + await addon_manager.async_create_backup() + + assert addon_info.call_count == 1 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + + +async def test_create_backup_error( + hass: HomeAssistant, + addon_manager: AddonManager, + addon_info: AsyncMock, + addon_installed: AsyncMock, + create_backup: AsyncMock, +) -> None: + """Test creating a backup of the addon raises error.""" + create_backup.side_effect = HassioAPIError("Boom") + + with pytest.raises(AddonError) as err: + await addon_manager.async_create_backup() + + assert str(err.value) == "Failed to create a backup of the Test add-on: Boom" + + assert addon_info.call_count == 1 + assert create_backup.call_count == 1 + assert create_backup.call_args == call( + hass, {"name": "addon_test_addon_1.0.0", "addons": ["test_addon"]}, partial=True + ) + + +async def test_schedule_install_setup_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule install setup addon.""" + install_task = addon_manager.async_schedule_install_setup_addon( + {"test_key": "test"} + ) + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one install task is running. + install_task_two = addon_manager.async_schedule_install_setup_addon( + {"test_key": "test"} + ) + + await asyncio.gather(install_task, install_task_two) + + assert addon_manager.task_in_progress() is False + assert install_addon.call_count == 1 + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + install_addon.reset_mock() + set_addon_options.reset_mock() + start_addon.reset_mock() + + # Test that another call can be made after the install is done. + await addon_manager.async_schedule_install_setup_addon({"test_key": "test"}) + + assert install_addon.call_count == 1 + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + +@pytest.mark.parametrize( + ( + "install_addon_error, install_addon_calls, " + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_message" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + None, + 0, + "Failed to install the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_install_setup_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + install_addon_error: Exception | None, + install_addon_calls: int, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_message: str, +) -> None: + """Test schedule install setup addon raises error.""" + install_addon.side_effect = install_addon_error + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_install_setup_addon({"test_key": "test"}) + + assert str(err.value) == error_message + + assert install_addon.call_count == install_addon_calls + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls + + +@pytest.mark.parametrize( + ( + "install_addon_error, install_addon_calls, " + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_log" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + None, + 0, + "Failed to install the Test add-on: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_install_setup_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + install_addon: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + install_addon_error: Exception | None, + install_addon_calls: int, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_log: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule install setup addon logs error.""" + install_addon.side_effect = install_addon_error + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + await addon_manager.async_schedule_install_setup_addon( + {"test_key": "test"}, catch_error=True + ) + + assert error_log in caplog.text + assert install_addon.call_count == install_addon_calls + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls + + +async def test_schedule_setup_addon( + addon_manager: AddonManager, + addon_installed: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, +) -> None: + """Test schedule setup addon.""" + start_task = addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + assert addon_manager.task_in_progress() is True + + # Make sure that actually only one start task is running. + start_task_two = addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + await asyncio.gather(start_task, start_task_two) + + assert addon_manager.task_in_progress() is False + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + set_addon_options.reset_mock() + start_addon.reset_mock() + + # Test that another call can be made after the start is done. + await addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + assert set_addon_options.call_count == 1 + assert start_addon.call_count == 1 + + +@pytest.mark.parametrize( + ( + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_message" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_setup_addon_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_message: str, +) -> None: + """Test schedule setup addon raises error.""" + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + with pytest.raises(AddonError) as err: + await addon_manager.async_schedule_setup_addon({"test_key": "test"}) + + assert str(err.value) == error_message + + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls + + +@pytest.mark.parametrize( + ( + "set_addon_options_error, set_addon_options_calls, " + "start_addon_error, start_addon_calls, " + "error_log" + ), + [ + ( + HassioAPIError("Boom"), + 1, + None, + 0, + "Failed to set the Test add-on options: Boom", + ), + ( + None, + 1, + HassioAPIError("Boom"), + 1, + "Failed to start the Test add-on: Boom", + ), + ], +) +async def test_schedule_setup_addon_logs_error( + addon_manager: AddonManager, + addon_installed: AsyncMock, + set_addon_options: AsyncMock, + start_addon: AsyncMock, + set_addon_options_error: Exception | None, + set_addon_options_calls: int, + start_addon_error: Exception | None, + start_addon_calls: int, + error_log: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test schedule setup addon logs error.""" + set_addon_options.side_effect = set_addon_options_error + start_addon.side_effect = start_addon_error + + await addon_manager.async_schedule_setup_addon( + {"test_key": "test"}, catch_error=True + ) + + assert error_log in caplog.text + assert set_addon_options.call_count == set_addon_options_calls + assert start_addon.call_count == start_addon_calls diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 68052aeaab1daa..74b43e8394a30b 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -26,7 +26,7 @@ def addon_info_side_effect_fixture(): def mock_addon_info(addon_info_side_effect): """Mock Supervisor add-on info.""" with patch( - "homeassistant.components.zwave_js.addon.async_get_addon_info", + "homeassistant.components.hassio.addon_manager.async_get_addon_info", side_effect=addon_info_side_effect, ) as addon_info: addon_info.return_value = { @@ -48,7 +48,7 @@ def addon_store_info_side_effect_fixture(): def mock_addon_store_info(addon_store_info_side_effect): """Mock Supervisor add-on info.""" with patch( - "homeassistant.components.zwave_js.addon.async_get_addon_store_info", + "homeassistant.components.hassio.addon_manager.async_get_addon_store_info", side_effect=addon_store_info_side_effect, ) as addon_store_info: addon_store_info.return_value = { @@ -112,7 +112,7 @@ async def set_addon_options(hass, slug, options): def mock_set_addon_options(set_addon_options_side_effect): """Mock set add-on options.""" with patch( - "homeassistant.components.zwave_js.addon.async_set_addon_options", + "homeassistant.components.hassio.addon_manager.async_set_addon_options", side_effect=set_addon_options_side_effect, ) as set_options: yield set_options @@ -139,7 +139,7 @@ async def install_addon(hass, slug): def mock_install_addon(install_addon_side_effect): """Mock install add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_install_addon", + "homeassistant.components.hassio.addon_manager.async_install_addon", side_effect=install_addon_side_effect, ) as install_addon: yield install_addon @@ -149,7 +149,7 @@ def mock_install_addon(install_addon_side_effect): def mock_update_addon(): """Mock update add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_update_addon" + "homeassistant.components.hassio.addon_manager.async_update_addon" ) as update_addon: yield update_addon @@ -174,7 +174,7 @@ async def start_addon(hass, slug): def mock_start_addon(start_addon_side_effect): """Mock start add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_start_addon", + "homeassistant.components.hassio.addon_manager.async_start_addon", side_effect=start_addon_side_effect, ) as start_addon: yield start_addon @@ -184,7 +184,7 @@ def mock_start_addon(start_addon_side_effect): def stop_addon_fixture(): """Mock stop add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_stop_addon" + "homeassistant.components.hassio.addon_manager.async_stop_addon" ) as stop_addon: yield stop_addon @@ -199,7 +199,7 @@ def restart_addon_side_effect_fixture(): def mock_restart_addon(restart_addon_side_effect): """Mock restart add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_restart_addon", + "homeassistant.components.hassio.addon_manager.async_restart_addon", side_effect=restart_addon_side_effect, ) as restart_addon: yield restart_addon @@ -209,7 +209,7 @@ def mock_restart_addon(restart_addon_side_effect): def uninstall_addon_fixture(): """Mock uninstall add-on.""" with patch( - "homeassistant.components.zwave_js.addon.async_uninstall_addon" + "homeassistant.components.hassio.addon_manager.async_uninstall_addon" ) as uninstall_addon: yield uninstall_addon @@ -218,7 +218,7 @@ def uninstall_addon_fixture(): def create_backup_fixture(): """Mock create backup.""" with patch( - "homeassistant.components.zwave_js.addon.async_create_backup" + "homeassistant.components.hassio.addon_manager.async_create_backup" ) as create_backup: yield create_backup diff --git a/tests/components/zwave_js/test_addon.py b/tests/components/zwave_js/test_addon.py deleted file mode 100644 index 45f732c1aa260a..00000000000000 --- a/tests/components/zwave_js/test_addon.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tests for Z-Wave JS addon module.""" -import pytest - -from homeassistant.components.zwave_js.addon import AddonError, get_addon_manager -from homeassistant.components.zwave_js.const import ( - CONF_ADDON_DEVICE, - CONF_ADDON_S0_LEGACY_KEY, - CONF_ADDON_S2_ACCESS_CONTROL_KEY, - CONF_ADDON_S2_AUTHENTICATED_KEY, - CONF_ADDON_S2_UNAUTHENTICATED_KEY, -) - - -async def test_not_installed_raises_exception(hass, addon_not_installed): - """Test addon not installed raises exception.""" - addon_manager = get_addon_manager(hass) - - addon_config = { - CONF_ADDON_DEVICE: "/test", - CONF_ADDON_S0_LEGACY_KEY: "123", - CONF_ADDON_S2_ACCESS_CONTROL_KEY: "456", - CONF_ADDON_S2_AUTHENTICATED_KEY: "789", - CONF_ADDON_S2_UNAUTHENTICATED_KEY: "012", - } - - with pytest.raises(AddonError): - await addon_manager.async_configure_addon(addon_config) - - with pytest.raises(AddonError): - await addon_manager.async_update_addon() diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index f58b4187469108..eacf4b61cc8711 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -88,7 +88,7 @@ def discovery_info_side_effect_fixture(): def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect): """Mock get add-on discovery info.""" with patch( - "homeassistant.components.zwave_js.addon.async_get_addon_discovery_info", + "homeassistant.components.hassio.addon_manager.async_get_addon_discovery_info", side_effect=discovery_info_side_effect, return_value=discovery_info, ) as get_addon_discovery_info: From a954443795766624a888062b851e33f2d8a61bae Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Thu, 10 Nov 2022 10:32:07 +0100 Subject: [PATCH 360/394] Fix string typos and consistencies in nibe_heatpump (#81902) * Fix typos and consistency * Fix typos and consistency --- .../components/nibe_heatpump/strings.json | 12 ++++++------ .../components/nibe_heatpump/translations/en.json | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/nibe_heatpump/strings.json b/homeassistant/components/nibe_heatpump/strings.json index d6e93af689ae81..a863b9596b11e7 100644 --- a/homeassistant/components/nibe_heatpump/strings.json +++ b/homeassistant/components/nibe_heatpump/strings.json @@ -6,7 +6,7 @@ "nibegw": "NibeGW", "modbus": "Modbus" }, - "description": "Pick the connection method to your pump. In general, F-series pumps require a Nibe GW custom accessory, while an S-series pump has Modbus support built-in." + "description": "Pick the connection method to your pump. In general, F-series pumps require a NibeGW custom accessory, while an S-series pump has Modbus support built-in." }, "modbus": { "data": { @@ -16,7 +16,7 @@ }, "data_description": { "modbus_url": "Modbus URL that describes the connection to your Heat Pump or MODBUS40 unit. It should be on the form:\n - `tcp://[HOST]:[PORT]` for Modbus TCP connection\n - `serial://[LOCAL DEVICE]` for a local Modbus RTU connection\n - `rfc2217://[HOST]:[PORT]` for a remote telnet based Modbus RTU connection.", - "modbus_unit": "Unit identification for you Heat Pump. Can usually be left at 0." + "modbus_unit": "Unit identification for your Heat Pump. Can usually be left at 0." } }, "nibegw": { @@ -37,13 +37,13 @@ } }, "error": { - "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`.", - "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", + "write": "Error on write request to pump. Verify your `Remote write port` or `Remote address`.", + "read": "Error on read request from pump. Verify your `Remote read port` or `Remote address`.", "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.", "address_in_use": "The selected listening port is already in use on this system.", - "model": "The model selected doesn't seem to support modbus40", + "model": "The selected model doesn't seem to support MODBUS40", "unknown": "[%key:common::config_flow::error::unknown%]", - "url": "The url specified is not a well formed and supported url" + "url": "The specified URL is not well formed nor supported" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json index a662743c460523..afe50efb61ab7a 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -6,11 +6,11 @@ "error": { "address": "Invalid remote address specified. Address must be an IP address or a resolvable hostname.", "address_in_use": "The selected listening port is already in use on this system.", - "model": "The model selected doesn't seem to support modbus40", - "read": "Error on read request from pump. Verify your `Remote read port` or `Remote IP address`.", + "model": "The selected model doesn't seem to support MODBUS40", + "read": "Error on read request from pump. Verify your `Remote read port` or `Remote address`.", "unknown": "Unexpected error", - "url": "The url specified is not a well formed and supported url", - "write": "Error on write request to pump. Verify your `Remote write port` or `Remote IP address`." + "url": "The specified URL is not well formed nor supported", + "write": "Error on write request to pump. Verify your `Remote write port` or `Remote address`." }, "step": { "modbus": { @@ -20,7 +20,7 @@ "model": "Model of Heat Pump" }, "data_description": { - "modbus_unit": "Unit identification for you Heat Pump. Can usually be left at 0.", + "modbus_unit": "Unit identification for your Heat Pump. Can usually be left at 0.", "modbus_url": "Modbus URL that describes the connection to your Heat Pump or MODBUS40 unit. It should be on the form:\n - `tcp://[HOST]:[PORT]` for Modbus TCP connection\n - `serial://[LOCAL DEVICE]` for a local Modbus RTU connection\n - `rfc2217://[HOST]:[PORT]` for a remote telnet based Modbus RTU connection." } }, @@ -53,7 +53,7 @@ "remote_read_port": "The port the NibeGW unit is listening for read requests on.", "remote_write_port": "The port the NibeGW unit is listening for write requests on." }, - "description": "Pick the connection method to your pump. In general, F-series pumps require a Nibe GW custom accessory, while an S-series pump has Modbus support built-in.", + "description": "Pick the connection method to your pump. In general, F-series pumps require a NibeGW custom accessory, while an S-series pump has Modbus support built-in.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" @@ -61,4 +61,4 @@ } } } -} \ No newline at end of file +} From 874ece195e6064bd8f4585e7b936cd5ee1c3f673 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Nov 2022 13:19:21 +0100 Subject: [PATCH 361/394] Include config entry id in response to WS API hardware/info (#81906) --- homeassistant/components/hardkernel/hardware.py | 5 +++++ homeassistant/components/hardware/models.py | 1 + .../components/homeassistant_sky_connect/hardware.py | 1 + homeassistant/components/homeassistant_yellow/hardware.py | 7 +++++++ homeassistant/components/raspberry_pi/hardware.py | 5 +++++ tests/components/hardkernel/test_hardware.py | 1 + .../components/homeassistant_sky_connect/test_hardware.py | 2 ++ tests/components/homeassistant_yellow/test_hardware.py | 1 + tests/components/raspberry_pi/test_hardware.py | 1 + 9 files changed, 24 insertions(+) diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py index 47ff5830a84567..eca599960f8354 100644 --- a/homeassistant/components/hardkernel/hardware.py +++ b/homeassistant/components/hardkernel/hardware.py @@ -27,6 +27,10 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: if not board.startswith("odroid"): raise HomeAssistantError + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + return [ HardwareInfo( board=BoardInfo( @@ -35,6 +39,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: model=board, revision=None, ), + config_entries=config_entries, dongle=None, name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), url=None, diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 8ce5e7be7f33dd..801bc9b923acf3 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -34,6 +34,7 @@ class HardwareInfo: name: str | None board: BoardInfo | None + config_entries: list[str] | None dongle: USBInfo | None url: str | None diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index 6eceb7467569b6..f48e1763dd5714 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -17,6 +17,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: return [ HardwareInfo( board=None, + config_entries=[entry.entry_id], dongle=USBInfo( vid=entry.data["vid"], pid=entry.data["pid"], diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index ad17eccfe7fabb..b67eb50ff2ce5d 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -6,6 +6,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from .const import DOMAIN + BOARD_NAME = "Home Assistant Yellow" MANUFACTURER = "homeassistant" MODEL = "yellow" @@ -22,6 +24,10 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: if not board == "yellow": raise HomeAssistantError + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + return [ HardwareInfo( board=BoardInfo( @@ -30,6 +36,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: model=MODEL, revision=None, ), + config_entries=config_entries, dongle=None, name=BOARD_NAME, url=None, diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index 6433b15adb527c..61417f751acfeb 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -42,6 +42,10 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: if not board.startswith("rpi"): raise HomeAssistantError + config_entries = [ + entry.entry_id for entry in hass.config_entries.async_entries(DOMAIN) + ] + return [ HardwareInfo( board=BoardInfo( @@ -50,6 +54,7 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]: model=MODELS.get(board), revision=None, ), + config_entries=config_entries, dongle=None, name=BOARD_NAMES.get(board, f"Unknown Raspberry Pi model '{board}'"), url=None, diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py index 33602f92e3f9ee..e35c94e49268cd 100644 --- a/tests/components/hardkernel/test_hardware.py +++ b/tests/components/hardkernel/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "odroid-n2", "revision": None, }, + "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Blue / Hardkernel Odroid-N2", "url": None, diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 6226651133a73a..2ba305afb4a41f 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -62,6 +62,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "hardware": [ { "board": None, + "config_entries": [config_entry.entry_id], "dongle": { "vid": "bla_vid", "pid": "bla_pid", @@ -74,6 +75,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: }, { "board": None, + "config_entries": [config_entry_2.entry_id], "dongle": { "vid": "bla_vid_2", "pid": "bla_pid_2", diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 45d6fcabdfe9bc..681c031b2e1400 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "yellow", "revision": None, }, + "config_entries": [config_entry.entry_id], "dongle": None, "name": "Home Assistant Yellow", "url": None, diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index c36fcbd1642864..0f672ef98dbcd6 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "1", "revision": None, }, + "config_entries": [config_entry.entry_id], "dongle": None, "name": "Raspberry Pi", "url": None, From ee9231363ffee6f61e4be9b4a7ff125798365fe4 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 10 Nov 2022 14:25:41 +0100 Subject: [PATCH 362/394] Refactor KNX Config and Options flows (#80641) --- homeassistant/components/knx/config_flow.py | 508 +++++++++--------- homeassistant/components/knx/strings.json | 107 +++- .../components/knx/translations/en.json | 117 +++- tests/components/knx/test_config_flow.py | 474 +++++++--------- 4 files changed, 622 insertions(+), 584 deletions(-) diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index d6516d1d4ef1f3..3d046ecaeec121 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -1,6 +1,7 @@ """Config flow for KNX.""" from __future__ import annotations +from abc import ABC, abstractmethod from typing import Any, Final import voluptuous as vol @@ -10,13 +11,13 @@ from xknx.io.gateway_scanner import GatewayDescriptor, GatewayScanner from xknx.secure import load_key_ring -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowHandler, FlowResult from homeassistant.helpers import selector from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.helpers.typing import UNDEFINED from .const import ( CONF_KNX_AUTOMATIC, @@ -47,21 +48,25 @@ CONF_KNX_GATEWAY: Final = "gateway" CONF_MAX_RATE_LIMIT: Final = 60 -CONF_DEFAULT_LOCAL_IP: Final = "0.0.0.0" DEFAULT_ENTRY_DATA = KNXConfigEntryData( individual_address=XKNX.DEFAULT_ADDRESS, + local_ip=None, multicast_group=DEFAULT_MCAST_GRP, multicast_port=DEFAULT_MCAST_PORT, - state_updater=CONF_KNX_DEFAULT_STATE_UPDATER, rate_limit=CONF_KNX_DEFAULT_RATE_LIMIT, + route_back=False, + state_updater=CONF_KNX_DEFAULT_STATE_UPDATER, ) CONF_KNX_TUNNELING_TYPE: Final = "tunneling_type" -CONF_KNX_LABEL_TUNNELING_TCP: Final = "TCP" -CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure" -CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP" -CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode" +CONF_KNX_TUNNELING_TYPE_LABELS: Final = { + CONF_KNX_TUNNELING: "UDP (Tunnelling v1)", + CONF_KNX_TUNNELING_TCP: "TCP (Tunnelling v2)", + CONF_KNX_TUNNELING_TCP_SECURE: "Secure Tunnelling (TCP)", +} + +OPTION_MANUAL_TUNNEL: Final = "Manual" _IA_SELECTOR = selector.TextSelector() _IP_SELECTOR = selector.TextSelector() @@ -75,88 +80,111 @@ ) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a KNX config flow.""" +class KNXCommonFlow(ABC, FlowHandler): + """Base class for KNX flows.""" - VERSION = 1 + def __init__(self, initial_data: KNXConfigEntryData) -> None: + """Initialize KNXCommonFlow.""" + self.initial_data = initial_data + self._found_gateways: list[GatewayDescriptor] = [] + self._found_tunnels: list[GatewayDescriptor] = [] + self._selected_tunnel: GatewayDescriptor | None = None + self._tunneling_config: KNXConfigEntryData | None = None - _found_tunnels: list[GatewayDescriptor] - _selected_tunnel: GatewayDescriptor | None - _tunneling_config: KNXConfigEntryData | None - - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> KNXOptionsFlowHandler: - """Get the options flow for this handler.""" - return KNXOptionsFlowHandler(config_entry) + @abstractmethod + def finish_flow(self, new_entry_data: KNXConfigEntryData, title: str) -> FlowResult: + """Finish the flow.""" - async def async_step_user(self, user_input: dict | None = None) -> FlowResult: - """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - self._found_tunnels = [] - self._selected_tunnel = None - self._tunneling_config = None - return await self.async_step_type() - - async def async_step_type(self, user_input: dict | None = None) -> FlowResult: + async def async_step_connection_type( + self, user_input: dict | None = None + ) -> FlowResult: """Handle connection type configuration.""" if user_input is not None: connection_type = user_input[CONF_KNX_CONNECTION_TYPE] - if connection_type == CONF_KNX_AUTOMATIC: - entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData( - connection_type=CONF_KNX_AUTOMATIC - ) - return self.async_create_entry( - title=CONF_KNX_AUTOMATIC.capitalize(), - data=entry_data, - ) - if connection_type == CONF_KNX_ROUTING: return await self.async_step_routing() - if connection_type == CONF_KNX_TUNNELING and self._found_tunnels: + if connection_type == CONF_KNX_TUNNELING: + self._found_tunnels = [ + gateway + for gateway in self._found_gateways + if gateway.supports_tunnelling + ] + self._found_tunnels.sort( + key=lambda tunnel: tunnel.individual_address.raw + if tunnel.individual_address + else 0 + ) return await self.async_step_tunnel() - return await self.async_step_manual_tunnel() + # Automatic connection type + entry_data = KNXConfigEntryData(connection_type=CONF_KNX_AUTOMATIC) + return self.finish_flow( + new_entry_data=entry_data, + title=CONF_KNX_AUTOMATIC.capitalize(), + ) supported_connection_types = { CONF_KNX_TUNNELING: CONF_KNX_TUNNELING.capitalize(), CONF_KNX_ROUTING: CONF_KNX_ROUTING.capitalize(), } - if gateways := await scan_for_gateways(): + self._found_gateways = await scan_for_gateways() + if self._found_gateways: # add automatic at first position only if a gateway responded supported_connection_types = { CONF_KNX_AUTOMATIC: CONF_KNX_AUTOMATIC.capitalize() } | supported_connection_types - self._found_tunnels = [ - gateway for gateway in gateways if gateway.supports_tunnelling - ] fields = { vol.Required(CONF_KNX_CONNECTION_TYPE): vol.In(supported_connection_types) } - return self.async_show_form(step_id="type", data_schema=vol.Schema(fields)) + return self.async_show_form( + step_id="connection_type", data_schema=vol.Schema(fields) + ) async def async_step_tunnel(self, user_input: dict | None = None) -> FlowResult: """Select a tunnel from a list. Will be skipped if the gateway scan was unsuccessful or if only one gateway was found.""" if user_input is not None: + if user_input[CONF_KNX_GATEWAY] == OPTION_MANUAL_TUNNEL: + if self._found_tunnels: + self._selected_tunnel = self._found_tunnels[0] + return await self.async_step_manual_tunnel() + self._selected_tunnel = next( tunnel for tunnel in self._found_tunnels if user_input[CONF_KNX_GATEWAY] == str(tunnel) ) - return await self.async_step_manual_tunnel() + connection_type = ( + CONF_KNX_TUNNELING_TCP_SECURE + if self._selected_tunnel.tunnelling_requires_secure + else CONF_KNX_TUNNELING_TCP + if self._selected_tunnel.supports_tunnelling_tcp + else CONF_KNX_TUNNELING + ) + self._tunneling_config = KNXConfigEntryData( + host=self._selected_tunnel.ip_addr, + port=self._selected_tunnel.port, + route_back=False, + connection_type=connection_type, + ) + if connection_type == CONF_KNX_TUNNELING_TCP_SECURE: + return self.async_show_menu( + step_id="secure_tunneling", + menu_options=["secure_knxkeys", "secure_tunnel_manual"], + ) + return self.finish_flow( + new_entry_data=self._tunneling_config, + title=f"Tunneling @ {self._selected_tunnel}", + ) - # skip this step if the user has only one unique gateway. - if len(self._found_tunnels) == 1: - self._selected_tunnel = self._found_tunnels[0] + if not self._found_tunnels: return await self.async_step_manual_tunnel() errors: dict = {} - tunnels_repr = {str(tunnel) for tunnel in self._found_tunnels} - fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnels_repr)} + tunnel_options = [str(tunnel) for tunnel in self._found_tunnels] + tunnel_options.append(OPTION_MANUAL_TUNNEL) + fields = {vol.Required(CONF_KNX_GATEWAY): vol.In(tunnel_options)} return self.async_show_form( step_id="tunnel", data_schema=vol.Schema(fields), errors=errors @@ -182,61 +210,83 @@ async def async_step_manual_tunnel( if not errors: connection_type = user_input[CONF_KNX_TUNNELING_TYPE] - entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData( + self._tunneling_config = KNXConfigEntryData( host=_host, port=user_input[CONF_PORT], - route_back=( - connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK - ), + route_back=user_input[CONF_KNX_ROUTE_BACK], local_ip=_local_ip, - connection_type=( - CONF_KNX_TUNNELING_TCP - if connection_type == CONF_KNX_LABEL_TUNNELING_TCP - else CONF_KNX_TUNNELING - ), + connection_type=connection_type, ) - if connection_type == CONF_KNX_LABEL_TUNNELING_TCP_SECURE: - self._tunneling_config = entry_data + if connection_type == CONF_KNX_TUNNELING_TCP_SECURE: return self.async_show_menu( step_id="secure_tunneling", - menu_options=["secure_knxkeys", "secure_manual"], + menu_options=["secure_knxkeys", "secure_tunnel_manual"], ) - - return self.async_create_entry( + return self.finish_flow( + new_entry_data=self._tunneling_config, title=f"Tunneling @ {_host}", - data=entry_data, ) - connection_methods: list[str] = [ - CONF_KNX_LABEL_TUNNELING_TCP, - CONF_KNX_LABEL_TUNNELING_UDP, - CONF_KNX_LABEL_TUNNELING_TCP_SECURE, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - ] - ip_address = "" - port = DEFAULT_MCAST_PORT - if self._selected_tunnel is not None: + _reconfiguring_existing_tunnel = ( + self.initial_data.get(CONF_KNX_CONNECTION_TYPE) + in CONF_KNX_TUNNELING_TYPE_LABELS + ) + if ( # initial attempt on ConfigFlow or coming from automatic / routing + (isinstance(self, ConfigFlow) or not _reconfiguring_existing_tunnel) + and not user_input + and self._selected_tunnel is not None + ): # default to first found tunnel ip_address = self._selected_tunnel.ip_addr port = self._selected_tunnel.port - if not self._selected_tunnel.supports_tunnelling_tcp: - connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP) - connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP_SECURE) + if self._selected_tunnel.tunnelling_requires_secure: + default_type = CONF_KNX_TUNNELING_TCP_SECURE + elif self._selected_tunnel.supports_tunnelling_tcp: + default_type = CONF_KNX_TUNNELING_TCP + else: + default_type = CONF_KNX_TUNNELING + else: # OptionFlow, no tunnel discovered or user input + ip_address = ( + user_input[CONF_HOST] + if user_input + else self.initial_data.get(CONF_HOST) + ) + port = ( + user_input[CONF_PORT] + if user_input + else self.initial_data.get(CONF_PORT, DEFAULT_MCAST_PORT) + ) + default_type = ( + user_input[CONF_KNX_TUNNELING_TYPE] + if user_input + else self.initial_data[CONF_KNX_CONNECTION_TYPE] + if _reconfiguring_existing_tunnel + else CONF_KNX_TUNNELING + ) + _route_back: bool = self.initial_data.get( + CONF_KNX_ROUTE_BACK, not bool(self._selected_tunnel) + ) fields = { - vol.Required(CONF_KNX_TUNNELING_TYPE): vol.In(connection_methods), + vol.Required(CONF_KNX_TUNNELING_TYPE, default=default_type): vol.In( + CONF_KNX_TUNNELING_TYPE_LABELS + ), vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR, vol.Required(CONF_PORT, default=port): _PORT_SELECTOR, + vol.Required( + CONF_KNX_ROUTE_BACK, default=_route_back + ): selector.BooleanSelector(), } - if self.show_advanced_options: fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR + if not self._found_tunnels: + errors["base"] = "no_tunnel_discovered" return self.async_show_form( step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors ) - async def async_step_secure_manual( + async def async_step_secure_tunnel_manual( self, user_input: dict | None = None ) -> FlowResult: """Configure ip secure manually.""" @@ -250,14 +300,16 @@ async def async_step_secure_manual( user_id=user_input[CONF_KNX_SECURE_USER_ID], user_password=user_input[CONF_KNX_SECURE_USER_PASSWORD], ) - - return self.async_create_entry( + return self.finish_flow( + new_entry_data=entry_data, title=f"Secure Tunneling @ {self._tunneling_config[CONF_HOST]}", - data=entry_data, ) fields = { - vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All( + vol.Required( + CONF_KNX_SECURE_USER_ID, + default=self.initial_data.get(CONF_KNX_SECURE_USER_ID, 2), + ): vol.All( selector.NumberSelector( selector.NumberSelectorConfig( min=1, max=127, mode=selector.NumberSelectorMode.BOX @@ -265,16 +317,24 @@ async def async_step_secure_manual( ), vol.Coerce(int), ), - vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.TextSelector( + vol.Required( + CONF_KNX_SECURE_USER_PASSWORD, + default=self.initial_data.get(CONF_KNX_SECURE_USER_PASSWORD), + ): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD), ), - vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.TextSelector( + vol.Required( + CONF_KNX_SECURE_DEVICE_AUTHENTICATION, + default=self.initial_data.get(CONF_KNX_SECURE_DEVICE_AUTHENTICATION), + ): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD), ), } return self.async_show_form( - step_id="secure_manual", data_schema=vol.Schema(fields), errors=errors + step_id="secure_tunnel_manual", + data_schema=vol.Schema(fields), + errors=errors, ) async def async_step_secure_knxkeys( @@ -302,15 +362,20 @@ async def async_step_secure_knxkeys( knxkeys_filename=storage_key, knxkeys_password=user_input[CONF_KNX_KNXKEY_PASSWORD], ) - - return self.async_create_entry( + return self.finish_flow( + new_entry_data=entry_data, title=f"Secure Tunneling @ {self._tunneling_config[CONF_HOST]}", - data=entry_data, ) fields = { - vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.TextSelector(), - vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.TextSelector(), + vol.Required( + CONF_KNX_KNXKEY_FILENAME, + default=self.initial_data.get(CONF_KNX_KNXKEY_FILENAME), + ): selector.TextSelector(), + vol.Required( + CONF_KNX_KNXKEY_PASSWORD, + default=self.initial_data.get(CONF_KNX_KNXKEY_PASSWORD), + ): selector.TextSelector(), } return self.async_show_form( @@ -323,10 +388,17 @@ async def async_step_routing(self, user_input: dict | None = None) -> FlowResult _individual_address = ( user_input[CONF_KNX_INDIVIDUAL_ADDRESS] if user_input - else XKNX.DEFAULT_ADDRESS + else self.initial_data[CONF_KNX_INDIVIDUAL_ADDRESS] ) _multicast_group = ( - user_input[CONF_KNX_MCAST_GRP] if user_input else DEFAULT_MCAST_GRP + user_input[CONF_KNX_MCAST_GRP] + if user_input + else self.initial_data[CONF_KNX_MCAST_GRP] + ) + _multicast_port = ( + user_input[CONF_KNX_MCAST_PORT] + if user_input + else self.initial_data[CONF_KNX_MCAST_PORT] ) if user_input is not None: @@ -345,15 +417,16 @@ async def async_step_routing(self, user_input: dict | None = None) -> FlowResult errors[CONF_KNX_LOCAL_IP] = "invalid_ip_address" if not errors: - entry_data = DEFAULT_ENTRY_DATA | KNXConfigEntryData( + entry_data = KNXConfigEntryData( connection_type=CONF_KNX_ROUTING, individual_address=_individual_address, multicast_group=_multicast_group, - multicast_port=user_input[CONF_KNX_MCAST_PORT], + multicast_port=_multicast_port, local_ip=_local_ip, ) - return self.async_create_entry( - title=CONF_KNX_ROUTING.capitalize(), data=entry_data + return self.finish_flow( + new_entry_data=entry_data, + title=f"Routing as {_individual_address}", ) fields = { @@ -361,101 +434,112 @@ async def async_step_routing(self, user_input: dict | None = None) -> FlowResult CONF_KNX_INDIVIDUAL_ADDRESS, default=_individual_address ): _IA_SELECTOR, vol.Required(CONF_KNX_MCAST_GRP, default=_multicast_group): _IP_SELECTOR, - vol.Required( - CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT - ): _PORT_SELECTOR, + vol.Required(CONF_KNX_MCAST_PORT, default=_multicast_port): _PORT_SELECTOR, } if self.show_advanced_options: # Optional with default doesn't work properly in flow UI fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR + if not any( + router for router in self._found_gateways if router.supports_routing + ): + errors["base"] = "no_router_discovered" return self.async_show_form( step_id="routing", data_schema=vol.Schema(fields), errors=errors ) -class KNXOptionsFlowHandler(OptionsFlow): +class KNXConfigFlow(KNXCommonFlow, ConfigFlow, domain=DOMAIN): + """Handle a KNX config flow.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize KNX options flow.""" + super().__init__(initial_data=DEFAULT_ENTRY_DATA) + + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> KNXOptionsFlow: + """Get the options flow for this handler.""" + return KNXOptionsFlow(config_entry) + + @callback + def finish_flow(self, new_entry_data: KNXConfigEntryData, title: str) -> FlowResult: + """Create the ConfigEntry.""" + return self.async_create_entry( + title=title, + data=DEFAULT_ENTRY_DATA | new_entry_data, + ) + + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + """Handle a flow initialized by the user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + return await self.async_step_connection_type() + + +class KNXOptionsFlow(KNXCommonFlow, OptionsFlow): """Handle KNX options.""" general_settings: dict - current_config: dict def __init__(self, config_entry: ConfigEntry) -> None: """Initialize KNX options flow.""" self.config_entry = config_entry + super().__init__(initial_data=config_entry.data) # type: ignore[arg-type] + + @callback + def finish_flow( + self, new_entry_data: KNXConfigEntryData, title: str | None + ) -> FlowResult: + """Update the ConfigEntry and finish the flow.""" + new_data = DEFAULT_ENTRY_DATA | self.initial_data | new_entry_data + self.hass.config_entries.async_update_entry( + self.config_entry, + data=new_data, + title=title or UNDEFINED, + ) + return self.async_create_entry(title="", data={}) async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage KNX options.""" - if user_input is not None: - self.general_settings = user_input - return await self.async_step_tunnel() + return self.async_show_menu( + step_id="options_init", + menu_options=["connection_type", "communication_settings"], + ) - supported_connection_types = [ - CONF_KNX_AUTOMATIC, - CONF_KNX_TUNNELING, - CONF_KNX_ROUTING, - ] - self.current_config = self.config_entry.data # type: ignore[assignment] + async def async_step_communication_settings( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage KNX communication settings.""" + if user_input is not None: + return self.finish_flow( + new_entry_data=KNXConfigEntryData( + state_updater=user_input[CONF_KNX_STATE_UPDATER], + rate_limit=user_input[CONF_KNX_RATE_LIMIT], + ), + title=None, + ) data_schema = { vol.Required( - CONF_KNX_CONNECTION_TYPE, - default=( - CONF_KNX_TUNNELING - if self.current_config.get(CONF_KNX_CONNECTION_TYPE) - == CONF_KNX_TUNNELING_TCP - else self.current_config.get(CONF_KNX_CONNECTION_TYPE) + CONF_KNX_STATE_UPDATER, + default=self.initial_data.get( + CONF_KNX_STATE_UPDATER, + CONF_KNX_DEFAULT_STATE_UPDATER, ), - ): vol.In(supported_connection_types), - vol.Required( - CONF_KNX_INDIVIDUAL_ADDRESS, - default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS], - ): selector.TextSelector(), - vol.Required( - CONF_KNX_MCAST_GRP, - default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP), - ): _IP_SELECTOR, + ): selector.BooleanSelector(), vol.Required( - CONF_KNX_MCAST_PORT, - default=self.current_config.get( - CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT - ), - ): _PORT_SELECTOR, - } - - if self.show_advanced_options: - local_ip = ( - self.current_config.get(CONF_KNX_LOCAL_IP) - if self.current_config.get(CONF_KNX_LOCAL_IP) is not None - else CONF_DEFAULT_LOCAL_IP - ) - data_schema[ - vol.Required( - CONF_KNX_LOCAL_IP, - default=local_ip, - ) - ] = _IP_SELECTOR - data_schema[ - vol.Required( - CONF_KNX_STATE_UPDATER, - default=self.current_config.get( - CONF_KNX_STATE_UPDATER, - CONF_KNX_DEFAULT_STATE_UPDATER, - ), - ) - ] = selector.BooleanSelector() - data_schema[ - vol.Required( + CONF_KNX_RATE_LIMIT, + default=self.initial_data.get( CONF_KNX_RATE_LIMIT, - default=self.current_config.get( - CONF_KNX_RATE_LIMIT, - CONF_KNX_DEFAULT_RATE_LIMIT, - ), - ) - ] = vol.All( + CONF_KNX_DEFAULT_RATE_LIMIT, + ), + ): vol.All( selector.NumberSelector( selector.NumberSelectorConfig( min=0, @@ -464,96 +548,14 @@ async def async_step_init( ), ), vol.Coerce(int), - ) - + ), + } return self.async_show_form( - step_id="init", + step_id="communication_settings", data_schema=vol.Schema(data_schema), - last_step=self.current_config.get(CONF_KNX_CONNECTION_TYPE) - != CONF_KNX_TUNNELING, + last_step=True, ) - async def async_step_tunnel( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage KNX tunneling options.""" - if ( - self.general_settings.get(CONF_KNX_CONNECTION_TYPE) == CONF_KNX_TUNNELING - and user_input is None - ): - connection_methods: list[str] = [ - CONF_KNX_LABEL_TUNNELING_TCP, - CONF_KNX_LABEL_TUNNELING_UDP, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - ] - return self.async_show_form( - step_id="tunnel", - data_schema=vol.Schema( - { - vol.Required( - CONF_KNX_TUNNELING_TYPE, - default=get_knx_tunneling_type(self.current_config), - ): vol.In(connection_methods), - vol.Required( - CONF_HOST, default=self.current_config.get(CONF_HOST) - ): _IP_SELECTOR, - vol.Required( - CONF_PORT, default=self.current_config.get(CONF_PORT, 3671) - ): _PORT_SELECTOR, - } - ), - last_step=True, - ) - - _local_ip = self.general_settings.get(CONF_KNX_LOCAL_IP) - entry_data = ( - DEFAULT_ENTRY_DATA - | self.general_settings - | KNXConfigEntryData( - host=self.current_config.get(CONF_HOST, ""), - local_ip=_local_ip if _local_ip != CONF_DEFAULT_LOCAL_IP else None, - ) - ) - - if user_input is not None: - connection_type = user_input[CONF_KNX_TUNNELING_TYPE] - entry_data = entry_data | KNXConfigEntryData( - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - route_back=(connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK), - connection_type=( - CONF_KNX_TUNNELING_TCP - if connection_type == CONF_KNX_LABEL_TUNNELING_TCP - else CONF_KNX_TUNNELING - ), - ) - - entry_title = str(entry_data[CONF_KNX_CONNECTION_TYPE]).capitalize() - if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING: - entry_title = f"Tunneling @ {entry_data[CONF_HOST]}" - if entry_data[CONF_KNX_CONNECTION_TYPE] == CONF_KNX_TUNNELING_TCP: - entry_title = f"Tunneling @ {entry_data[CONF_HOST]} (TCP)" - - self.hass.config_entries.async_update_entry( - self.config_entry, - data=entry_data, - title=entry_title, - ) - - return self.async_create_entry(title="", data={}) - - -def get_knx_tunneling_type(config_entry_data: dict) -> str: - """Obtain the knx tunneling type based on the data in the config entry data.""" - connection_type = config_entry_data[CONF_KNX_CONNECTION_TYPE] - route_back = config_entry_data.get(CONF_KNX_ROUTE_BACK, False) - if route_back and connection_type == CONF_KNX_TUNNELING: - return CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK - if connection_type == CONF_KNX_TUNNELING_TCP: - return CONF_KNX_LABEL_TUNNELING_TCP - - return CONF_KNX_LABEL_TUNNELING_UDP - async def scan_for_gateways(stop_on_found: int = 0) -> list[GatewayDescriptor]: """Scan for gateways within the network.""" diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index c8161462d66b9b..d87ad6ac17776a 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -1,7 +1,7 @@ { "config": { "step": { - "type": { + "connection_type": { "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", "data": { "connection_type": "KNX Connection Type" @@ -19,11 +19,13 @@ "tunneling_type": "KNX Tunneling Type", "port": "[%key:common::config_flow::data::port%]", "host": "[%key:common::config_flow::data::host%]", + "route_back": "Route back / NAT mode", "local_ip": "Local IP of Home Assistant" }, "data_description": { "port": "Port of the KNX/IP tunneling device.", "host": "IP address of the KNX/IP tunneling device.", + "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections.", "local_ip": "Leave blank to use auto-discovery." } }, @@ -31,7 +33,7 @@ "description": "Select how you want to configure KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", - "secure_manual": "Configure IP secure keys manually" + "secure_tunnel_manual": "Configure IP secure keys manually" } }, "secure_knxkeys": { @@ -45,7 +47,7 @@ "knxkeys_password": "This was set when exporting the file from ETS." } }, - "secure_manual": { + "secure_tunnel_manual": { "description": "Please enter your IP secure information.", "data": { "user_id": "User ID", @@ -81,41 +83,110 @@ "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", "invalid_ip_address": "Invalid IPv4 address.", "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/" + "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", + "no_router_discovered": "No KNXnet/IP router was discovered on the network.", + "no_tunnel_discovered": "Could not find a KNX tunneling server on your network." } }, "options": { "step": { - "init": { + "options_init": { + "menu_options": { + "connection_type": "Configure KNX interface", + "communication_settings": "Communication settings" + } + }, + "communication_settings": { "data": { - "connection_type": "KNX Connection Type", - "individual_address": "Default individual address", - "multicast_group": "[%key:component::knx::config::step::routing::data::multicast_group%]", - "multicast_port": "[%key:component::knx::config::step::routing::data::multicast_port%]", - "local_ip": "Local IP of Home Assistant", "state_updater": "State updater", "rate_limit": "Rate limit" }, "data_description": { - "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", - "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", - "multicast_port": "Used for routing and discovery. Default: `3671`", - "local_ip": "Use `0.0.0.0` for auto-discovery.", "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options.", - "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40" + "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40" + } + }, + "connection_type": { + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing.", + "data": { + "connection_type": "KNX Connection Type" } }, "tunnel": { + "description": "[%key:component::knx::config::step::tunnel::description%]", "data": { - "tunneling_type": "KNX Tunneling Type", + "gateway": "[%key:component::knx::config::step::tunnel::data::gateway%]" + } + }, + "manual_tunnel": { + "description": "[%key:component::knx::config::step::manual_tunnel::description%]", + "data": { + "tunneling_type": "[%key:component::knx::config::step::manual_tunnel::data::tunneling_type%]", "port": "[%key:common::config_flow::data::port%]", - "host": "[%key:common::config_flow::data::host%]" + "host": "[%key:common::config_flow::data::host%]", + "route_back": "[%key:component::knx::config::step::manual_tunnel::data::route_back%]", + "local_ip": "[%key:component::knx::config::step::manual_tunnel::data::local_ip%]" }, "data_description": { "port": "[%key:component::knx::config::step::manual_tunnel::data_description::port%]", - "host": "[%key:component::knx::config::step::manual_tunnel::data_description::host%]" + "host": "[%key:component::knx::config::step::manual_tunnel::data_description::host%]", + "route_back": "[%key:component::knx::config::step::manual_tunnel::data_description::route_back%]", + "local_ip": "[%key:component::knx::config::step::manual_tunnel::data_description::local_ip%]" + } + }, + "secure_tunneling": { + "description": "[%key:component::knx::config::step::secure_tunneling::description%]", + "menu_options": { + "secure_knxkeys": "[%key:component::knx::config::step::secure_tunneling::menu_options::secure_knxkeys%]", + "secure_tunnel_manual": "[%key:component::knx::config::step::secure_tunneling::menu_options::secure_tunnel_manual%]" + } + }, + "secure_knxkeys": { + "description": "[%key:component::knx::config::step::secure_knxkeys::description%]", + "data": { + "knxkeys_filename": "[%key:component::knx::config::step::secure_knxkeys::data::knxkeys_filename%]", + "knxkeys_password": "[%key:component::knx::config::step::secure_knxkeys::data::knxkeys_password%]" + }, + "data_description": { + "knxkeys_filename": "[%key:component::knx::config::step::secure_knxkeys::data_description::knxkeys_filename%]", + "knxkeys_password": "[%key:component::knx::config::step::secure_knxkeys::data_description::knxkeys_password%]" + } + }, + "secure_tunnel_manual": { + "description": "[%key:component::knx::config::step::secure_tunnel_manual::description%]", + "data": { + "user_id": "[%key:component::knx::config::step::secure_tunnel_manual::data::user_id%]", + "user_password": "[%key:component::knx::config::step::secure_tunnel_manual::data::user_password%]", + "device_authentication": "[%key:component::knx::config::step::secure_tunnel_manual::data::device_authentication%]" + }, + "data_description": { + "user_id": "[%key:component::knx::config::step::secure_tunnel_manual::data_description::user_id%]", + "user_password": "[%key:component::knx::config::step::secure_tunnel_manual::data_description::user_password%]", + "device_authentication": "[%key:component::knx::config::step::secure_tunnel_manual::data_description::device_authentication%]" + } + }, + "routing": { + "description": "[%key:component::knx::config::step::routing::description%]", + "data": { + "individual_address": "[%key:component::knx::config::step::routing::data::individual_address%]", + "multicast_group": "[%key:component::knx::config::step::routing::data::multicast_group%]", + "multicast_port": "[%key:component::knx::config::step::routing::data::multicast_port%]", + "local_ip": "[%key:component::knx::config::step::routing::data::local_ip%]" + }, + "data_description": { + "individual_address": "[%key:component::knx::config::step::routing::data_description::individual_address%]", + "local_ip": "[%key:component::knx::config::step::routing::data_description::local_ip%]" } } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_individual_address": "[%key:component::knx::config::error::invalid_individual_address%]", + "invalid_ip_address": "[%key:component::knx::config::error::invalid_ip_address%]", + "invalid_signature": "[%key:component::knx::config::error::invalid_signature%]", + "file_not_found": "[%key:component::knx::config::error::file_not_found%]", + "no_router_discovered": "[%key:component::knx::config::error::no_router_discovered%]", + "no_tunnel_discovered": "[%key:component::knx::config::error::no_tunnel_discovered%]" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 6dffe059b2a037..c45c98b070a17f 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -9,20 +9,30 @@ "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", "invalid_ip_address": "Invalid IPv4 address.", - "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong." + "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", + "no_router_discovered": "No KNXnet/IP router was discovered on the network.", + "no_tunnel_discovered": "Could not find a KNX tunneling server on your network." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX Connection Type" + }, + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "Local IP of Home Assistant", "port": "Port", + "route_back": "Route back / NAT mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { "host": "IP address of the KNX/IP tunneling device.", "local_ip": "Leave blank to use auto-discovery.", - "port": "Port of the KNX/IP tunneling device." + "port": "Port of the KNX/IP tunneling device.", + "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections." }, "description": "Please enter the connection information of your tunneling device." }, @@ -50,7 +60,7 @@ }, "description": "Please enter the information for your `.knxkeys` file." }, - "secure_manual": { + "secure_tunnel_manual": { "data": { "device_authentication": "Device authentication password", "user_id": "User ID", @@ -67,7 +77,7 @@ "description": "Select how you want to configure KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", - "secure_manual": "Configure IP secure keys manually" + "secure_tunnel_manual": "Configure IP secure keys manually" } }, "tunnel": { @@ -75,46 +85,107 @@ "gateway": "KNX Tunnel Connection" }, "description": "Please select a gateway from the list." - }, - "type": { - "data": { - "connection_type": "KNX Connection Type" - }, - "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." } } }, "options": { + "error": { + "cannot_connect": "Failed to connect", + "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", + "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", + "invalid_ip_address": "Invalid IPv4 address.", + "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", + "no_router_discovered": "No KNXnet/IP router was discovered on the network.", + "no_tunnel_discovered": "Could not find a KNX tunneling server on your network." + }, "step": { - "init": { + "communication_settings": { "data": { - "connection_type": "KNX Connection Type", - "individual_address": "Default individual address", - "local_ip": "Local IP of Home Assistant", - "multicast_group": "Multicast group", - "multicast_port": "Multicast port", "rate_limit": "Rate limit", "state_updater": "State updater" }, "data_description": { - "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", - "local_ip": "Use `0.0.0.0` for auto-discovery.", - "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", - "multicast_port": "Used for routing and discovery. Default: `3671`", - "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40", + "rate_limit": "Maximum outgoing telegrams per second.\n`0` to disable limit. Recommended: 0 or 20 to 40", "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options." } }, - "tunnel": { + "connection_type": { + "data": { + "connection_type": "KNX Connection Type" + }, + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." + }, + "manual_tunnel": { "data": { "host": "Host", + "local_ip": "Local IP of Home Assistant", "port": "Port", + "route_back": "Route back / NAT mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { "host": "IP address of the KNX/IP tunneling device.", - "port": "Port of the KNX/IP tunneling device." + "local_ip": "Leave blank to use auto-discovery.", + "port": "Port of the KNX/IP tunneling device.", + "route_back": "Enable if your KNXnet/IP tunneling server is behind NAT. Only applies for UDP connections." + }, + "description": "Please enter the connection information of your tunneling device." + }, + "options_init": { + "menu_options": { + "communication_settings": "Communication settings", + "connection_type": "Configure KNX interface" + } + }, + "routing": { + "data": { + "individual_address": "Individual address", + "local_ip": "Local IP of Home Assistant", + "multicast_group": "Multicast group", + "multicast_port": "Multicast port" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Leave blank to use auto-discovery." + }, + "description": "Please configure the routing options." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "The filename of your `.knxkeys` file (including extension)", + "knxkeys_password": "The password to decrypt the `.knxkeys` file" + }, + "data_description": { + "knxkeys_filename": "The file is expected to be found in your config directory in `.storage/knx/`.\nIn Home Assistant OS this would be `/config/.storage/knx/`\nExample: `my_project.knxkeys`", + "knxkeys_password": "This was set when exporting the file from ETS." + }, + "description": "Please enter the information for your `.knxkeys` file." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Device authentication password", + "user_id": "User ID", + "user_password": "User password" + }, + "data_description": { + "device_authentication": "This is set in the 'IP' panel of the interface in ETS.", + "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", + "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS." + }, + "description": "Please enter your IP secure information." + }, + "secure_tunneling": { + "description": "Select how you want to configure KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_tunnel_manual": "Configure IP secure keys manually" } + }, + "tunnel": { + "data": { + "gateway": "KNX Tunnel Connection" + }, + "description": "Please select a gateway from the list." } } } diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 30b8aa537a6edb..fdef15ed217ed7 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -3,24 +3,20 @@ import pytest from xknx.exceptions.exception import InvalidSignature -from xknx.io import DEFAULT_MCAST_GRP +from xknx.io import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT from xknx.io.gateway_scanner import GatewayDescriptor from homeassistant import config_entries from homeassistant.components.knx.config_flow import ( - CONF_DEFAULT_LOCAL_IP, CONF_KNX_GATEWAY, - CONF_KNX_LABEL_TUNNELING_TCP, - CONF_KNX_LABEL_TUNNELING_TCP_SECURE, - CONF_KNX_LABEL_TUNNELING_UDP, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, CONF_KNX_TUNNELING_TYPE, DEFAULT_ENTRY_DATA, - get_knx_tunneling_type, + OPTION_MANUAL_TUNNEL, ) from homeassistant.components.knx.const import ( CONF_KNX_AUTOMATIC, CONF_KNX_CONNECTION_TYPE, + CONF_KNX_DEFAULT_STATE_UPDATER, CONF_KNX_INDIVIDUAL_ADDRESS, CONF_KNX_KNXKEY_FILENAME, CONF_KNX_KNXKEY_PASSWORD, @@ -41,16 +37,19 @@ ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry def _gateway_descriptor( - ip: str, port: int, supports_tunnelling_tcp: bool = False + ip: str, + port: int, + supports_tunnelling_tcp: bool = False, + requires_secure: bool = False, ) -> GatewayDescriptor: """Get mock gw descriptor.""" - return GatewayDescriptor( + descriptor = GatewayDescriptor( name="Test", ip_addr=ip, port=port, @@ -60,6 +59,9 @@ def _gateway_descriptor( supports_tunnelling=True, supports_tunnelling_tcp=supports_tunnelling_tcp, ) + descriptor.tunnelling_requires_secure = requires_secure + descriptor.routing_requires_secure = requires_secure + return descriptor async def test_user_single_instance(hass): @@ -92,7 +94,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_router_discovered"} with patch( "homeassistant.components.knx.async_setup_entry", @@ -108,7 +110,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == CONF_KNX_ROUTING.capitalize() + assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, @@ -144,7 +146,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_router_discovered"} # invalid user input result_invalid_input = await hass.config_entries.flow.async_configure( @@ -163,6 +165,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: CONF_KNX_MCAST_GRP: "invalid_ip_address", CONF_KNX_INDIVIDUAL_ADDRESS: "invalid_individual_address", CONF_KNX_LOCAL_IP: "invalid_ip_address", + "base": "no_router_discovered", } # valid user input @@ -181,7 +184,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == CONF_KNX_ROUTING.capitalize() + assert result3["title"] == "Routing as 1.1.110" assert result3["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_ROUTING, @@ -199,9 +202,10 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: [ ( { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, + CONF_KNX_ROUTE_BACK: False, }, { **DEFAULT_ENTRY_DATA, @@ -215,9 +219,10 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: ), ( { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_TCP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, + CONF_KNX_ROUTE_BACK: False, }, { **DEFAULT_ENTRY_DATA, @@ -231,9 +236,10 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: ), ( { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, + CONF_KNX_ROUTE_BACK: True, }, { **DEFAULT_ENTRY_DATA, @@ -247,13 +253,12 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: ), ], ) -async def test_tunneling_setup( +async def test_tunneling_setup_manual( hass: HomeAssistant, user_input, config_entry_data ) -> None: - """Test tunneling if only one gateway is found.""" - gateway = _gateway_descriptor("192.168.0.1", 3675, True) + """Test tunneling if no gateway was found found (or `manual` option was chosen).""" with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] + gateways.return_value = [] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -269,7 +274,7 @@ async def test_tunneling_setup( await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_tunnel_discovered"} with patch( "homeassistant.components.knx.async_setup_entry", @@ -289,9 +294,8 @@ async def test_tunneling_setup( async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: """Test tunneling if only one gateway is found.""" - gateway = _gateway_descriptor("192.168.0.2", 3675) with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] + gateways.return_value = [] result = await hass.config_entries.flow.async_init( DOMAIN, context={ @@ -311,13 +315,13 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" - assert not result2["errors"] + assert result2["errors"] == {"base": "no_tunnel_discovered"} # invalid host ip address result_invalid_host = await hass.config_entries.flow.async_configure( result2["flow_id"], { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: DEFAULT_MCAST_GRP, # multicast addresses are invalid CONF_PORT: 3675, CONF_KNX_LOCAL_IP: "192.168.1.112", @@ -326,12 +330,15 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result_invalid_host["type"] == FlowResultType.FORM assert result_invalid_host["step_id"] == "manual_tunnel" - assert result_invalid_host["errors"] == {CONF_HOST: "invalid_ip_address"} + assert result_invalid_host["errors"] == { + CONF_HOST: "invalid_ip_address", + "base": "no_tunnel_discovered", + } # invalid local ip address result_invalid_local = await hass.config_entries.flow.async_configure( result2["flow_id"], { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.2", CONF_PORT: 3675, CONF_KNX_LOCAL_IP: "asdf", @@ -340,7 +347,10 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result_invalid_local["type"] == FlowResultType.FORM assert result_invalid_local["step_id"] == "manual_tunnel" - assert result_invalid_local["errors"] == {CONF_KNX_LOCAL_IP: "invalid_ip_address"} + assert result_invalid_local["errors"] == { + CONF_KNX_LOCAL_IP: "invalid_ip_address", + "base": "no_tunnel_discovered", + } # valid user input with patch( @@ -350,7 +360,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.2", CONF_PORT: 3675, CONF_KNX_LOCAL_IP: "192.168.1.112", @@ -395,29 +405,17 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] - manual_tunnel = await hass.config_entries.flow.async_configure( - tunnel_flow["flow_id"], - {CONF_KNX_GATEWAY: str(gateway)}, - ) - await hass.async_block_till_done() - assert manual_tunnel["type"] == FlowResultType.FORM - assert manual_tunnel["step_id"] == "manual_tunnel" - with patch( "homeassistant.components.knx.async_setup_entry", return_value=True, ) as mock_setup_entry: - manual_tunnel_flow = await hass.config_entries.flow.async_configure( - manual_tunnel["flow_id"], - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, - CONF_HOST: "192.168.0.1", - CONF_PORT: 3675, - }, + result = await hass.config_entries.flow.async_configure( + tunnel_flow["flow_id"], + {CONF_KNX_GATEWAY: str(gateway)}, ) await hass.async_block_till_done() - assert manual_tunnel_flow["type"] == FlowResultType.CREATE_ENTRY - assert manual_tunnel_flow["data"] == { + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, CONF_HOST: "192.168.0.1", @@ -430,10 +428,22 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) assert len(mock_setup_entry.mock_calls) == 1 -async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: - """Test manual tunnel if no gateway is found and tunneling is selected.""" +@pytest.mark.parametrize( + "gateway", + [ + _gateway_descriptor("192.168.0.1", 3675), + _gateway_descriptor("192.168.0.1", 3675, supports_tunnelling_tcp=True), + _gateway_descriptor( + "192.168.0.1", 3675, supports_tunnelling_tcp=True, requires_secure=True + ), + ], +) +async def test_manual_tunnel_step_with_found_gateway( + hass: HomeAssistant, gateway +) -> None: + """Test manual tunnel if gateway was found and tunneling is selected.""" with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [] + gateways.return_value = [gateway] result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -448,9 +458,20 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert tunnel_flow["type"] == FlowResultType.FORM - assert tunnel_flow["step_id"] == "manual_tunnel" + assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] + manual_tunnel_flow = await hass.config_entries.flow.async_configure( + tunnel_flow["flow_id"], + { + CONF_KNX_GATEWAY: OPTION_MANUAL_TUNNEL, + }, + ) + await hass.async_block_till_done() + assert manual_tunnel_flow["type"] == FlowResultType.FORM + assert manual_tunnel_flow["step_id"] == "manual_tunnel" + assert not manual_tunnel_flow["errors"] + async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -484,9 +505,14 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N assert len(mock_setup_entry.mock_calls) == 1 -async def _get_menu_step(hass: HomeAssistant) -> None: - """Test ip secure manuel.""" - gateway = _gateway_descriptor("192.168.0.1", 3675, True) +async def _get_menu_step(hass: HomeAssistant) -> FlowResult: + """Return flow in secure_tunnellinn menu step.""" + gateway = _gateway_descriptor( + "192.168.0.1", + 3675, + supports_tunnelling_tcp=True, + requires_secure=True, + ) with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: gateways.return_value = [gateway] result = await hass.config_entries.flow.async_init( @@ -503,13 +529,59 @@ async def _get_menu_step(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "manual_tunnel" + assert result2["step_id"] == "tunnel" assert not result2["errors"] result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], + {CONF_KNX_GATEWAY: str(gateway)}, + ) + await hass.async_block_till_done() + assert result3["type"] == FlowResultType.MENU + assert result3["step_id"] == "secure_tunneling" + return result3 + + +async def test_get_secure_menu_step_manual_tunnelling( + hass: HomeAssistant, +): + """Test flow reaches secure_tunnellinn menu step from manual tunnelling configuration.""" + gateway = _gateway_descriptor( + "192.168.0.1", + 3675, + supports_tunnelling_tcp=True, + requires_secure=True, + ) + with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: + gateways.return_value = [gateway] + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert not result["errors"] + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_TCP_SECURE, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + }, + ) + await hass.async_block_till_done() + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "tunnel" + assert not result2["errors"] + + manual_tunnel_flow = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_KNX_GATEWAY: OPTION_MANUAL_TUNNEL, + }, + ) + + result3 = await hass.config_entries.flow.async_configure( + manual_tunnel_flow["flow_id"], + { + CONF_KNX_TUNNELING_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, CONF_HOST: "192.168.0.1", CONF_PORT: 3675, }, @@ -517,26 +589,25 @@ async def _get_menu_step(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result3["type"] == FlowResultType.MENU assert result3["step_id"] == "secure_tunneling" - return result3 -async def test_configure_secure_manual(hass: HomeAssistant): - """Test configure secure manual.""" +async def test_configure_secure_tunnel_manual(hass: HomeAssistant): + """Test configure tunnelling secure keys manually.""" menu_step = await _get_menu_step(hass) result = await hass.config_entries.flow.async_configure( menu_step["flow_id"], - {"next_step_id": "secure_manual"}, + {"next_step_id": "secure_tunnel_manual"}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "secure_manual" + assert result["step_id"] == "secure_tunnel_manual" assert not result["errors"] with patch( "homeassistant.components.knx.async_setup_entry", return_value=True, ) as mock_setup_entry: - secure_manual = await hass.config_entries.flow.async_configure( + secure_tunnel_manual = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_KNX_SECURE_USER_ID: 2, @@ -545,8 +616,8 @@ async def test_configure_secure_manual(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_manual["type"] == FlowResultType.CREATE_ENTRY - assert secure_manual["data"] == { + assert secure_tunnel_manual["type"] == FlowResultType.CREATE_ENTRY + assert secure_tunnel_manual["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, CONF_KNX_SECURE_USER_ID: 2, @@ -662,265 +733,88 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature" -async def test_options_flow( +async def test_options_flow_connection_type( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: - """Test options config flow.""" + """Test options flow changing interface.""" mock_config_entry.add_to_hass(hass) - gateway = _gateway_descriptor("192.168.0.1", 3675) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] - result = await hass.config_entries.options.async_init( - mock_config_entry.entry_id - ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert "flow_id" in result - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - }, - ) - - await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY - assert not result2.get("data") - assert mock_config_entry.data == { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_HOST: "", - CONF_KNX_LOCAL_IP: None, - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - } - - -@pytest.mark.parametrize( - "user_input,config_entry_data", - [ - ( - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - CONF_KNX_LOCAL_IP: None, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - CONF_KNX_ROUTE_BACK: True, - }, - ), - ( - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_UDP, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - CONF_KNX_LOCAL_IP: None, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - CONF_KNX_ROUTE_BACK: False, - }, - ), - ( - { - CONF_KNX_TUNNELING_TYPE: CONF_KNX_LABEL_TUNNELING_TCP, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 20, - CONF_KNX_STATE_UPDATER: True, - CONF_KNX_LOCAL_IP: None, - CONF_HOST: "192.168.1.1", - CONF_PORT: 3675, - CONF_KNX_ROUTE_BACK: False, - }, - ), - ], -) -async def test_tunneling_options_flow( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - user_input, - config_entry_data, -) -> None: - """Test options flow for tunneling.""" - mock_config_entry.add_to_hass(hass) + menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - gateway = _gateway_descriptor("192.168.0.1", 3675) with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: gateways.return_value = [gateway] - result = await hass.config_entries.options.async_init( - mock_config_entry.entry_id + result = await hass.config_entries.options.async_configure( + menu_step["flow_id"], + {"next_step_id": "connection_type"}, ) assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert "flow_id" in result + assert result.get("step_id") == "connection_type" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.255", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, }, ) - assert result2.get("type") == FlowResultType.FORM - assert not result2.get("data") - assert "flow_id" in result2 + assert result2.get("step_id") == "tunnel" result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input=user_input, + user_input={ + CONF_KNX_GATEWAY: str(gateway), + }, ) - await hass.async_block_till_done() assert result3.get("type") == FlowResultType.CREATE_ENTRY assert not result3.get("data") - assert mock_config_entry.data == config_entry_data + assert mock_config_entry.data == { + CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, + CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", + CONF_HOST: "192.168.0.1", + CONF_PORT: 3675, + CONF_KNX_LOCAL_IP: None, + CONF_KNX_MCAST_PORT: DEFAULT_MCAST_PORT, + CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, + CONF_KNX_RATE_LIMIT: 20, + CONF_KNX_STATE_UPDATER: CONF_KNX_DEFAULT_STATE_UPDATER, + CONF_KNX_ROUTE_BACK: False, + } -@pytest.mark.parametrize( - "user_input,config_entry_data", - [ - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: "192.168.1.112", - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_HOST: "", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: "192.168.1.112", - }, - ), - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: CONF_DEFAULT_LOCAL_IP, - }, - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, - CONF_KNX_INDIVIDUAL_ADDRESS: "15.15.250", - CONF_HOST: "", - CONF_KNX_MCAST_PORT: 3675, - CONF_KNX_MCAST_GRP: DEFAULT_MCAST_GRP, - CONF_KNX_RATE_LIMIT: 25, - CONF_KNX_STATE_UPDATER: False, - CONF_KNX_LOCAL_IP: None, - }, - ), - ], -) -async def test_advanced_options( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - user_input, - config_entry_data, +async def test_options_communication_settings( + hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: - """Test options config flow.""" + """Test options flow changing communication settings.""" mock_config_entry.add_to_hass(hass) - gateway = _gateway_descriptor("192.168.0.1", 3675) - with patch("xknx.io.gateway_scanner.GatewayScanner.scan") as gateways: - gateways.return_value = [gateway] - result = await hass.config_entries.options.async_init( - mock_config_entry.entry_id, context={"show_advanced_options": True} - ) - - assert result.get("type") == FlowResultType.FORM - assert result.get("step_id") == "init" - assert "flow_id" in result + menu_step = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input=user_input, - ) - - await hass.async_block_till_done() - assert result2.get("type") == FlowResultType.CREATE_ENTRY - assert not result2.get("data") + result = await hass.config_entries.options.async_configure( + menu_step["flow_id"], + {"next_step_id": "communication_settings"}, + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "communication_settings" - assert mock_config_entry.data == config_entry_data + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_RATE_LIMIT: 0, + }, + ) + await hass.async_block_till_done() + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert not result2.get("data") -@pytest.mark.parametrize( - "config_entry_data,result", - [ - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_ROUTE_BACK: False, - }, - CONF_KNX_LABEL_TUNNELING_UDP, - ), - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, - CONF_KNX_ROUTE_BACK: True, - }, - CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK, - ), - ( - { - CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP, - CONF_KNX_ROUTE_BACK: False, - }, - CONF_KNX_LABEL_TUNNELING_TCP, - ), - ], -) -async def test_get_knx_tunneling_type( - config_entry_data, - result, -) -> None: - """Test converting config entry data to tunneling type for config flow.""" - assert get_knx_tunneling_type(config_entry_data) == result + assert mock_config_entry.data == { + **DEFAULT_ENTRY_DATA, + CONF_KNX_CONNECTION_TYPE: CONF_KNX_AUTOMATIC, + CONF_KNX_STATE_UPDATER: False, + CONF_KNX_RATE_LIMIT: 0, + } From 9bd676aff644aab1419458f80ace8bc11a806b8a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Nov 2022 14:44:55 +0100 Subject: [PATCH 363/394] Improve automation reload (#81854) * Improve automation reload * Small tweak * Improve --- .../components/automation/__init__.py | 21 ++++++++- tests/components/automation/test_init.py | 43 ++++++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 234fcc978397a5..9581a6b1c40019 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -817,9 +817,28 @@ def find_matches( """ automation_matches: set[int] = set() config_matches: set[int] = set() + automation_configs_with_id: dict[str, tuple[int, AutomationEntityConfig]] = {} + automation_configs_without_id: list[tuple[int, AutomationEntityConfig]] = [] + + for config_idx, config in enumerate(automation_configs): + if automation_id := config.config_block.get(CONF_ID): + automation_configs_with_id[automation_id] = (config_idx, config) + continue + automation_configs_without_id.append((config_idx, config)) for automation_idx, automation in enumerate(automations): - for config_idx, config in enumerate(automation_configs): + if automation.unique_id: + if automation.unique_id not in automation_configs_with_id: + continue + config_idx, config = automation_configs_with_id.pop( + automation.unique_id + ) + if automation_matches_config(automation, config): + automation_matches.add(automation_idx) + config_matches.add(config_idx) + continue + + for config_idx, config in automation_configs_without_id: if config_idx in config_matches: # Only allow an automation config to match at most once continue diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index f40309bf7f6eff..742ae85ed68183 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -738,7 +738,8 @@ def running_cb(event): assert len(calls) == (1 if service == "turn_off_no_stop" else 0) -async def test_reload_unchanged_does_not_stop(hass, calls): +@pytest.mark.parametrize("extra_config", ({}, {"id": "sun"})) +async def test_reload_unchanged_does_not_stop(hass, calls, extra_config): """Test that reloading stops any running actions as appropriate.""" test_entity = "test.entity" @@ -753,6 +754,7 @@ async def test_reload_unchanged_does_not_stop(hass, calls): ], } } + config[automation.DOMAIN].update(**extra_config) assert await async_setup_component(hass, automation.DOMAIN, config) running = asyncio.Event() @@ -970,6 +972,41 @@ async def test_reload_identical_automations_without_id(hass, calls): }, } }, + { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "test.automation"}], + }, + # An automation using templates + { + "id": "sun", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": [{"service": "{{ 'test.automation' }}"}], + }, + # An automation using blueprint + { + "id": "sun", + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "test_event", + "service_to_call": "test.automation", + "a_number": 5, + }, + }, + }, + # An automation using blueprint with templated input + { + "id": "sun", + "use_blueprint": { + "path": "test_event_service.yaml", + "input": { + "trigger_event": "{{ 'test_event' }}", + "service_to_call": "{{ 'test.automation' }}", + "a_number": 5, + }, + }, + }, ), ) async def test_reload_unchanged_automation(hass, calls, automation_config): @@ -1004,7 +1041,8 @@ async def test_reload_unchanged_automation(hass, calls, automation_config): assert len(calls) == 2 -async def test_reload_automation_when_blueprint_changes(hass, calls): +@pytest.mark.parametrize("extra_config", ({}, {"id": "sun"})) +async def test_reload_automation_when_blueprint_changes(hass, calls, extra_config): """Test an automation is updated at reload if the blueprint has changed.""" with patch( "homeassistant.components.automation.AutomationEntity", wraps=AutomationEntity @@ -1023,6 +1061,7 @@ async def test_reload_automation_when_blueprint_changes(hass, calls): } ] } + config[automation.DOMAIN][0].update(**extra_config) assert await async_setup_component(hass, automation.DOMAIN, config) assert automation_entity_init.call_count == 1 automation_entity_init.reset_mock() From 7500d0c61cfea3508bca9c53c144c0bc9c035142 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 10 Nov 2022 15:24:56 +0100 Subject: [PATCH 364/394] Refactor MQTT_WILL_BIRTH_SCHEMA (#81879) * Refactor MQTT_WILL_BIRTH_SCHEMA * Refactor and move birth/will validation to utils * Simplify birth will validation --- homeassistant/components/mqtt/__init__.py | 4 +-- homeassistant/components/mqtt/config.py | 4 +-- homeassistant/components/mqtt/config_flow.py | 6 ++--- .../components/mqtt/config_integration.py | 26 ++++--------------- homeassistant/components/mqtt/util.py | 21 +++++++++++---- 5 files changed, 28 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 06921105aae0aa..f7c9e5fbe864cb 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -95,12 +95,12 @@ ReceivePayloadType, ) from .util import ( - _VALID_QOS_SCHEMA, async_create_certificate_temp_files, get_mqtt_data, migrate_certificate_file_to_content, mqtt_config_entry_enabled, valid_publish_topic, + valid_qos_schema, valid_subscribe_topic, ) @@ -172,7 +172,7 @@ vol.Exclusive(ATTR_TOPIC_TEMPLATE, CONF_TOPIC): cv.string, vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True, diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 8cfc3490f0cd03..88adcac71941a7 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -16,10 +16,10 @@ DEFAULT_QOS, DEFAULT_RETAIN, ) -from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic +from .util import valid_publish_topic, valid_qos_schema, valid_subscribe_topic SCHEMA_BASE = { - vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(CONF_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, } diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index ec8183487012fa..5d8b19ce31ae2f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -71,9 +71,9 @@ SUPPORTED_PROTOCOLS, ) from .util import ( - MQTT_WILL_BIRTH_SCHEMA, async_create_certificate_temp_files, get_file_path, + valid_birth_will, valid_publish_topic, ) @@ -326,7 +326,7 @@ def _validate( CONF_BIRTH_MESSAGE, _birth_will("birth"), "bad_birth", - MQTT_WILL_BIRTH_SCHEMA, + valid_birth_will, ) if not user_input["birth_enable"]: options_config[CONF_BIRTH_MESSAGE] = {} @@ -336,7 +336,7 @@ def _validate( CONF_WILL_MESSAGE, _birth_will("will"), "bad_will", - MQTT_WILL_BIRTH_SCHEMA, + valid_birth_will, ) if not user_input["will_enable"]: options_config[CONF_WILL_MESSAGE] = {} diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 2be125c2c1209b..9319a48ac3d14d 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -36,10 +36,6 @@ vacuum as vacuum_platform, ) from .const import ( - ATTR_PAYLOAD, - ATTR_QOS, - ATTR_RETAIN, - ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CERTIFICATE, @@ -56,12 +52,10 @@ DEFAULT_PORT, DEFAULT_PREFIX, DEFAULT_PROTOCOL, - DEFAULT_QOS, - DEFAULT_RETAIN, DEFAULT_WILL, SUPPORTED_PROTOCOLS, ) -from .util import _VALID_QOS_SCHEMA, valid_publish_topic +from .util import valid_birth_will, valid_publish_topic DEFAULT_TLS_PROTOCOL = "auto" @@ -144,16 +138,6 @@ "the MQTT broker configuration" ) -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - CONFIG_SCHEMA_ENTRY = vol.Schema( { vol.Optional(CONF_CLIENT_ID): cv.string, @@ -170,8 +154,8 @@ vol.Optional(CONF_TLS_INSECURE): cv.boolean, vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_WILL_MESSAGE): valid_birth_will, + vol.Optional(CONF_BIRTH_MESSAGE): valid_birth_will, vol.Optional(CONF_DISCOVERY): cv.boolean, # discovery_prefix must be a valid publish topic because if no # state topic is specified, it will be created with the given prefix. @@ -197,8 +181,8 @@ vol.Optional(CONF_TLS_INSECURE): cv.boolean, vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), vol.Optional(CONF_PROTOCOL): vol.All(cv.string, vol.In(SUPPORTED_PROTOCOLS)), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_WILL_MESSAGE): valid_birth_will, + vol.Optional(CONF_BIRTH_MESSAGE): valid_birth_will, vol.Optional(CONF_DISCOVERY): cv.boolean, # discovery_prefix must be a valid publish topic because if no # state topic is specified, it will be created with the given prefix. diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 0b2d10977aa334..ab907854499d3b 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -9,7 +9,6 @@ import voluptuous as vol -from homeassistant.const import CONF_PAYLOAD from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType @@ -32,6 +31,8 @@ TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" +_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) + def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None: """Return true when the MQTT config entry is enabled.""" @@ -112,19 +113,29 @@ def valid_publish_topic(topic: Any) -> str: return validated_topic -_VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) +def valid_qos_schema(qos: Any) -> int: + """Validate that QOS value is valid.""" + return _VALID_QOS_SCHEMA(qos) -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + +_MQTT_WILL_BIRTH_SCHEMA = vol.Schema( { vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Required(ATTR_PAYLOAD): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema, vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, required=True, ) +def valid_birth_will(config: ConfigType) -> ConfigType: + """Validate a birth or will configuration and required topic/payload.""" + if config: + config = _MQTT_WILL_BIRTH_SCHEMA(config) + return config + + def get_mqtt_data(hass: HomeAssistant, ensure_exists: bool = False) -> MqttData: """Return typed MqttData from hass.data[DATA_MQTT].""" if ensure_exists: From ae2b2acab5049a6f30615fa7ae3d4ef0848125e1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Nov 2022 17:25:42 +0100 Subject: [PATCH 365/394] Fix grammar in tts service description (#81916) --- homeassistant/components/tts/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 0e0c41e5e30ef5..5914512a3157ea 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -240,7 +240,7 @@ async def async_say_handle(service: ServiceCall) -> None: # Register the service description service_desc = { - CONF_NAME: f"Say an TTS message with {p_type}", + CONF_NAME: f"Say a TTS message with {p_type}", CONF_DESCRIPTION: f"Say something using text-to-speech on a media player with {p_type}.", CONF_FIELDS: services_dict[SERVICE_SAY][CONF_FIELDS], } From 2f9982d1c70f2d9fdd25de2c87a2565961fe027c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Nov 2022 17:27:14 +0100 Subject: [PATCH 366/394] Fix race when deleting a script (#81897) --- homeassistant/components/config/script.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 45a7a6dc227e24..73f89aaf509d92 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -16,9 +16,8 @@ async def async_setup(hass): async def hook(action, config_key): """post_write_hook for Config View that reloads scripts.""" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) - if action != ACTION_DELETE: + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) return ent_reg = er.async_get(hass) From 25d54f407e65647c331e1269f2ed2acd624c9fbb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Nov 2022 17:27:26 +0100 Subject: [PATCH 367/394] Fix race when deleting a scene (#81896) --- homeassistant/components/config/scene.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 862c8c46f4d8bb..befbfd052af4e3 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -15,9 +15,8 @@ async def async_setup(hass): async def hook(action, config_key): """post_write_hook for Config View that reloads scenes.""" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) - if action != ACTION_DELETE: + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) return ent_reg = entity_registry.async_get(hass) From a2da1c7db56747049d513e599bbf3be20bfa64c5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Nov 2022 17:28:19 +0100 Subject: [PATCH 368/394] Create repairs issue if an outdated currency code is configured in core store (#81772) * Create repairs issue if an outdated currency code is configured in core store * Update homeassistant/config.py Co-authored-by: Aarni Koskela Co-authored-by: Aarni Koskela --- homeassistant/config.py | 34 +++++++++++++++++----------------- tests/test_config.py | 25 ++++++++++++++++++++----- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 8e06c2c47a2ed0..c58f94ca1971eb 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -48,14 +48,9 @@ LEGACY_CONF_WHITELIST_EXTERNAL_DIRS, __version__, ) -from .core import ( - DOMAIN as CONF_CORE, - ConfigSource, - HomeAssistant, - async_get_hass, - callback, -) +from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback from .exceptions import HomeAssistantError +from .generated.currencies import HISTORIC_CURRENCIES from .helpers import ( config_per_platform, config_validation as cv, @@ -208,22 +203,25 @@ def _filter_bad_internal_external_urls(conf: dict) -> dict: ) +def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None: + if currency in HISTORIC_CURRENCIES: + ir.async_create_issue( + hass, + "homeassistant", + "historic_currency", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="historic_currency", + translation_placeholders={"currency": currency}, + ) + + def _validate_currency(data: Any) -> Any: - hass = async_get_hass() try: return cv.currency(data) except vol.InInvalid: with suppress(vol.InInvalid): currency = cv.historic_currency(data) - ir.async_create_issue( - hass, - "homeassistant", - "historic_currency", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="historic_currency", - translation_placeholders={"currency": currency}, - ) return currency raise @@ -580,6 +578,8 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non if key in config: setattr(hac, attr, config[key]) + _raise_issue_if_historic_currency(hass, hass.config.currency) + if CONF_TIME_ZONE in config: hac.set_time_zone(config[CONF_TIME_ZONE]) diff --git a/tests/test_config.py b/tests/test_config.py index 75ad227a641821..ef3646387250b2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1207,13 +1207,28 @@ def test_identify_config_schema(domain, schema, expected): ) -def test_core_config_schema_historic_currency(hass): +async def test_core_config_schema_historic_currency(hass): """Test core config schema.""" - config_util.CORE_CONFIG_SCHEMA( - { + await config_util.async_process_ha_core_config(hass, {"currency": "LTT"}) + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue("homeassistant", "historic_currency") + assert issue + assert issue.translation_placeholders == {"currency": "LTT"} + + +async def test_core_store_historic_currency(hass, hass_storage): + """Test core config store.""" + core_data = { + "data": { "currency": "LTT", - } - ) + }, + "key": "core.config", + "version": 1, + "minor_version": 1, + } + hass_storage["core.config"] = dict(core_data) + await config_util.async_process_ha_core_config(hass, {}) issue_registry = ir.async_get(hass) issue = issue_registry.async_get_issue("homeassistant", "historic_currency") From e6d1a4a4224c2c2332ccb5834aec474e7e126af8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 10 Nov 2022 08:31:28 -0800 Subject: [PATCH 369/394] Revert google calendar back to old API for free/busy readers (#81894) * Revert google calendar back to old API for free/busy readers * Update homeassistant/components/google/calendar.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/google/calendar.py | 10 ++++++++-- tests/components/google/conftest.py | 14 +++++++++++--- tests/components/google/test_calendar.py | 21 ++++++++++++++++----- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 4eb57cff49c114..eff26c2fbc4dc7 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -10,7 +10,7 @@ from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest from gcal_sync.exceptions import ApiException -from gcal_sync.model import DateOrDatetime, Event +from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.timeline import Timeline @@ -198,7 +198,13 @@ async def async_setup_entry( entity_entry.entity_id, ) coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator - if search := data.get(CONF_SEARCH): + # Prefer calendar sync down of resources when possible. However, sync does not work + # for search. Also free-busy calendars denormalize recurring events as individual + # events which is not efficient for sync + if ( + search := data.get(CONF_SEARCH) + or calendar_item.access_role == AccessRole.FREE_BUSY_READER + ): coordinator = CalendarQueryUpdateCoordinator( hass, calendar_service, diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 2f5efd829bf983..ad27e971eceb01 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -47,7 +47,6 @@ "id": CALENDAR_ID, "etag": '"3584134138943410"', "timeZone": "UTC", - "accessRole": "reader", "foregroundColor": "#000000", "selected": True, "kind": "calendar#calendarListEntry", @@ -62,10 +61,19 @@ CLIENT_SECRET = "client-secret" +@pytest.fixture(name="calendar_access_role") +def test_calendar_access_role() -> str: + """Default access role to use for test_api_calendar in tests.""" + return "reader" + + @pytest.fixture -def test_api_calendar(): +def test_api_calendar(calendar_access_role: str): """Return a test calendar object used in API responses.""" - return TEST_API_CALENDAR + return { + **TEST_API_CALENDAR, + "accessRole": calendar_access_role, + } @pytest.fixture diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 79eff393cc7ae5..0e53642548d413 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -60,6 +60,14 @@ } +@pytest.fixture( + autouse=True, scope="module", params=["reader", "owner", "freeBusyReader"] +) +def calendar_access_role(request) -> str: + """Fixture to exercise access roles in tests.""" + return request.param + + @pytest.fixture(autouse=True) def mock_test_setup( test_api_calendar, @@ -716,12 +724,15 @@ async def test_invalid_unique_id_cleanup( @pytest.mark.parametrize( - "time_zone,event_order", + "time_zone,event_order,calendar_access_role", + # This only tests the reader role to force testing against the local + # database filtering based on start/end time. (free busy reader would + # just use the API response which this test is not exercising) [ - ("America/Los_Angeles", ["One", "Two", "All Day Event"]), - ("America/Regina", ["One", "Two", "All Day Event"]), - ("UTC", ["One", "All Day Event", "Two"]), - ("Asia/Tokyo", ["All Day Event", "One", "Two"]), + ("America/Los_Angeles", ["One", "Two", "All Day Event"], "reader"), + ("America/Regina", ["One", "Two", "All Day Event"], "reader"), + ("UTC", ["One", "All Day Event", "Two"], "reader"), + ("Asia/Tokyo", ["All Day Event", "One", "Two"], "reader"), ], ) async def test_all_day_iter_order( From 97ebe59584b70c95d2aaf002607a0424239dffac Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 10 Nov 2022 19:33:10 +0100 Subject: [PATCH 370/394] Use UnitOfTemperature in devolo Home Control (#81923) --- homeassistant/components/devolo_home_control/climate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index 6c566aa45e30eb..227b479688384d 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -8,13 +8,12 @@ from homeassistant.components.climate import ( ATTR_TEMPERATURE, - TEMP_CELSIUS, ClimateEntity, ClimateEntityFeature, HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -68,7 +67,7 @@ def __init__( self._attr_precision = PRECISION_TENTHS self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE self._attr_target_temperature_step = PRECISION_HALVES - self._attr_temperature_unit = TEMP_CELSIUS + self._attr_temperature_unit = UnitOfTemperature.CELSIUS @property def current_temperature(self) -> float | None: From 281310141865eda58fda8ba81d489d843eda13f7 Mon Sep 17 00:00:00 2001 From: cdheiser <10488026+cdheiser@users.noreply.github.com> Date: Thu, 10 Nov 2022 10:35:47 -0800 Subject: [PATCH 371/394] Add cdheiser to Lutron codeowners (#81922) * Update manifest.json Add cdheiser as codeowner * Update CODEOWNERS Add @cdheiser as owner of Lutron. --- CODEOWNERS | 1 + homeassistant/components/lutron/manifest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1f45098e4f81a0..93715c3f93b5e7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -655,6 +655,7 @@ build.json @home-assistant/supervisor /homeassistant/components/luftdaten/ @fabaff @frenck /tests/components/luftdaten/ @fabaff @frenck /homeassistant/components/lupusec/ @majuss +/homeassistant/components/lutron/ @cdheiser /homeassistant/components/lutron_caseta/ @swails @bdraco @danaues /tests/components/lutron_caseta/ @swails @bdraco @danaues /homeassistant/components/lyric/ @timmo001 diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index d46a47cf38d7ea..cc002539d6b697 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": ["pylutron==0.2.8"], - "codeowners": [], + "codeowners": ["@cdheiser"], "iot_class": "local_polling", "loggers": ["pylutron"] } From 78180b2ad869fd5f1add1e0738024e59f53d9570 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Nov 2022 14:14:37 -0600 Subject: [PATCH 372/394] Fix bluetooth adapters with missing firmware patch files not being discovered (#81926) --- .../components/bluetooth/__init__.py | 25 +++++- homeassistant/components/bluetooth/const.py | 9 +++ tests/components/bluetooth/test_init.py | 77 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 1d0b8824fb5beb..8590d1ad90a341 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from asyncio import Future from collections.abc import Callable, Iterable +import datetime import logging import platform from typing import TYPE_CHECKING, cast @@ -21,6 +22,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -33,6 +35,7 @@ ADAPTER_ADDRESS, ADAPTER_HW_VERSION, ADAPTER_SW_VERSION, + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, @@ -40,6 +43,7 @@ DEFAULT_ADDRESS, DOMAIN, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, AdapterDetails, ) @@ -298,9 +302,17 @@ async def _async_rediscover_adapters() -> None: await async_discover_adapters(hass, discovered_adapters) discovery_debouncer = Debouncer( - hass, _LOGGER, cooldown=5, immediate=False, function=_async_rediscover_adapters + hass, + _LOGGER, + cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, + immediate=False, + function=_async_rediscover_adapters, ) + async def _async_call_debouncer(now: datetime.datetime) -> None: + """Call the debouncer at a later time.""" + await discovery_debouncer.async_call() + def _async_trigger_discovery() -> None: # There are so many bluetooth adapter models that # we check the bus whenever a usb device is plugged in @@ -310,6 +322,17 @@ def _async_trigger_discovery() -> None: # present. _LOGGER.debug("Triggering bluetooth usb discovery") hass.async_create_task(discovery_debouncer.async_call()) + # Because it can take 120s for the firmware loader + # fallback to timeout we need to wait that plus + # the debounce time to ensure we do not miss the + # adapter becoming available to DBus since otherwise + # we will never see the new adapter until + # Home Assistant is restarted + async_call_later( + hass, + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, + _async_call_debouncer, + ) cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery) hass.bus.async_listen_once( diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 6d6751f6ac4941..038c2b1988f573 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -59,6 +59,15 @@ SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30) +# When the linux kernel is configured with +# CONFIG_FW_LOADER_USER_HELPER_FALLBACK it +# can take up to 120s before the USB device +# is available if the firmware files +# are not present +LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS = 120 +BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS = 5 + + class AdapterDetails(TypedDict, total=False): """Adapter details.""" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index c9a5e6c78a7e81..5a5437af71ae6d 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -20,9 +20,11 @@ scanner, ) from homeassistant.components.bluetooth.const import ( + BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_PASSIVE, DEFAULT_ADDRESS, DOMAIN, + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -2737,6 +2739,81 @@ def _async_register_scan_request_callback(_hass, _callback): assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 +async def test_discover_new_usb_adapters_with_firmware_fallback_delay( + hass, mock_bleak_scanner_start, one_adapter +): + """Test we can discover new usb adapters with a firmware fallback delay.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_scan_request_callback(_hass, _callback): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.usb.async_register_scan_request_callback", + _async_register_scan_request_callback, + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + + assert not hass.config_entries.flow.async_progress(DOMAIN) + + saved_callback() + assert not hass.config_entries.flow.async_progress(DOMAIN) + + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 0 + + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + "hci1": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:02", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta( + seconds=LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS + + (BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS * 2) + ), + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress(DOMAIN)) == 1 + + async def test_issue_outdated_haos( hass, mock_bleak_scanner_start, one_adapter, operating_system_85 ): From f67ecd8ef58de2327b58027c7ae8048919a3a7d2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 10 Nov 2022 14:32:49 -0700 Subject: [PATCH 373/394] Bump aioridwell to 2022.11.0 (#81929) --- homeassistant/components/ridwell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index aec0faf5dd3f3b..785457a57e019a 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -3,7 +3,7 @@ "name": "Ridwell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", - "requirements": ["aioridwell==2022.03.0"], + "requirements": ["aioridwell==2022.11.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["aioridwell"], diff --git a/requirements_all.txt b/requirements_all.txt index f789ab61f0398b..17351f986a27c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -252,7 +252,7 @@ aioqsw==0.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.03.0 +aioridwell==2022.11.0 # homeassistant.components.senseme aiosenseme==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f70d132de0a88..4a4276019e99e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -227,7 +227,7 @@ aioqsw==0.2.2 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.03.0 +aioridwell==2022.11.0 # homeassistant.components.senseme aiosenseme==0.6.1 From 5621dfe4196990ad5591c51829f4b3eb8b3cf2e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 10 Nov 2022 17:07:30 -0600 Subject: [PATCH 374/394] Small cleanups for HomeKit Controller (#81933) --- .../homekit_controller/connection.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 320df67114458f..3df5ab8aaed54e 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Iterable from datetime import timedelta import logging from types import MappingProxyType @@ -110,10 +110,6 @@ def __init__( self.signal_state_updated = "_".join((DOMAIN, self.unique_id, "state_updated")) - # Current values of all characteristics homekit_controller is tracking. - # Key is a (accessory_id, characteristic_id) tuple. - self.current_state: dict[tuple[int, int], Any] = {} - self.pollable_characteristics: list[tuple[int, int]] = [] # If this is set polling is active and can be disabled by calling @@ -685,28 +681,26 @@ async def async_update(self, now=None): _LOGGER.debug("Finished HomeKit controller update: %s", self.unique_id) - def process_new_events(self, new_values_dict) -> None: + def process_new_events( + self, new_values_dict: dict[tuple[int, int], dict[str, Any]] + ) -> None: """Process events from accessory into HA state.""" self.async_set_available_state(True) # Process any stateless events (via device_triggers) async_fire_triggers(self, new_values_dict) - for (aid, cid), value in new_values_dict.items(): - accessory = self.current_state.setdefault(aid, {}) - accessory[cid] = value - - # self.current_state will be replaced by entity_map in a future PR - # For now we update both self.entity_map.process_changes(new_values_dict) async_dispatcher_send(self.hass, self.signal_state_updated) - async def get_characteristics(self, *args, **kwargs) -> dict[str, Any]: + async def get_characteristics(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Read latest state from homekit accessory.""" return await self.pairing.get_characteristics(*args, **kwargs) - async def put_characteristics(self, characteristics) -> None: + async def put_characteristics( + self, characteristics: Iterable[tuple[int, int, Any]] + ) -> None: """Control a HomeKit device state from Home Assistant.""" results = await self.pairing.put_characteristics(characteristics) From df0ba28b0552e0e29858ce6cd6756b5fc170bf40 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 11 Nov 2022 00:30:00 +0000 Subject: [PATCH 375/394] [ci skip] Translation update --- .../components/foscam/translations/bg.json | 1 + .../components/generic/translations/bg.json | 3 +- .../group/translations/zh-Hant.json | 8 +- .../components/knx/translations/ca.json | 125 ++++++++++++++++- .../components/knx/translations/de.json | 125 ++++++++++++++++- .../components/knx/translations/en.json | 48 ++++++- .../components/knx/translations/es.json | 131 +++++++++++++++++- .../components/knx/translations/et.json | 125 ++++++++++++++++- .../components/knx/translations/hu.json | 125 ++++++++++++++++- .../components/knx/translations/id.json | 125 ++++++++++++++++- .../components/knx/translations/it.json | 129 ++++++++++++++++- .../components/knx/translations/pt-BR.json | 125 ++++++++++++++++- .../components/knx/translations/zh-Hant.json | 127 ++++++++++++++++- .../nibe_heatpump/translations/ca.json | 16 +++ .../nibe_heatpump/translations/de.json | 10 +- .../nibe_heatpump/translations/en.json | 2 +- .../nibe_heatpump/translations/es.json | 12 +- .../nibe_heatpump/translations/et.json | 6 +- .../nibe_heatpump/translations/id.json | 17 ++- .../nibe_heatpump/translations/it.json | 12 +- .../nibe_heatpump/translations/pl.json | 8 +- .../nibe_heatpump/translations/pt-BR.json | 8 +- .../nibe_heatpump/translations/ru.json | 8 +- .../nibe_heatpump/translations/zh-Hant.json | 8 +- .../components/openuv/translations/bg.json | 1 + 25 files changed, 1215 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/foscam/translations/bg.json b/homeassistant/components/foscam/translations/bg.json index e660bd80f539c9..8318b416845ce5 100644 --- a/homeassistant/components/foscam/translations/bg.json +++ b/homeassistant/components/foscam/translations/bg.json @@ -10,6 +10,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "rtsp_port": "RTSP \u043f\u043e\u0440\u0442", + "stream": "\u041f\u043e\u0442\u043e\u043a", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/generic/translations/bg.json b/homeassistant/components/generic/translations/bg.json index 8c6944af94a8d5..b26b65e715e95d 100644 --- a/homeassistant/components/generic/translations/bg.json +++ b/homeassistant/components/generic/translations/bg.json @@ -23,7 +23,8 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - } + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u043a\u0430\u043c\u0435\u0440\u0430\u0442\u0430." }, "user_confirm_still": { "data": { diff --git a/homeassistant/components/group/translations/zh-Hant.json b/homeassistant/components/group/translations/zh-Hant.json index 023c76ebbba172..327aa3a0def76b 100644 --- a/homeassistant/components/group/translations/zh-Hant.json +++ b/homeassistant/components/group/translations/zh-Hant.json @@ -8,7 +8,7 @@ "hide_members": "\u96b1\u85cf\u6210\u54e1", "name": "\u540d\u7a31" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", "title": "\u65b0\u589e\u7fa4\u7d44" }, "cover": { @@ -82,7 +82,7 @@ "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" }, "cover": { "data": { @@ -102,7 +102,7 @@ "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" }, "lock": { "data": { @@ -122,7 +122,7 @@ "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u72c0\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" } } }, diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index ff83ebec0702e4..5b984ff4987830 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -9,20 +9,30 @@ "file_not_found": "No s'ha trobat el fitxer `.knxkeys` especificat a la ruta config/.storage/knx/", "invalid_individual_address": "El valor no coincideix amb el patr\u00f3 d'adre\u00e7a KNX individual.\n'area.line.device'", "invalid_ip_address": "Adre\u00e7a IPv4 inv\u00e0lida.", - "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta." + "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", + "no_router_discovered": "No s'ha descobert cap encaminador ('router') KNXnet/IP a la xarxa.", + "no_tunnel_discovered": "No s'ha trobat cap servidor de tunelitzaci\u00f3 KNX a la xarxa." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipus de connexi\u00f3 KNX" + }, + "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament." + }, "manual_tunnel": { "data": { "host": "Amfitri\u00f3", "local_ip": "IP local de Home Assistant", "port": "Port", + "route_back": "Encaminament de retorn / Mode NAT", "tunneling_type": "Tipus de t\u00fanel KNX" }, "data_description": { "host": "Adre\u00e7a IP del dispositiu de tunelitzaci\u00f3 KNX/IP.", "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic.", - "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP." + "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP.", + "route_back": "Activa-ho si el teun servidor de tunelitzaci\u00f3 KNXnet/IP est\u00e0 darrere una NAT. Nom\u00e9s s'aplica a connexions UDP." }, "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del dispositiu de t\u00fanel." }, @@ -63,11 +73,25 @@ }, "description": "Introdueix la informaci\u00f3 de seguretat IP (IP Secure)." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Contrasenya d'autenticaci\u00f3 del dispositiu", + "user_id": "ID d'usuari", + "user_password": "Contrasenya d'usuari" + }, + "data_description": { + "device_authentication": "S'estableix al panell 'IP' de la interf\u00edcie d'ETS.", + "user_id": "Sovint \u00e9s el n\u00famero del t\u00fanel +1. Per tant, 'T\u00fanel 2' tindria l'ID d'usuari '3'.", + "user_password": "Contrasenya per a la connexi\u00f3 t\u00fanel espec\u00edfica configurada al panell 'Propietats' del t\u00fanel a ETS." + }, + "description": "Introdueix la informaci\u00f3 de seguretat IP (IP Secure)." + }, "secure_tunneling": { "description": "Selecciona com vols configurar KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Utilitza un fitxer `.knxkeys` que contingui les claus de seguretat IP (IP Secure)", - "secure_manual": "Configura manualment les claus de seguretat IP (IP Secure)" + "secure_manual": "Configura manualment les claus de seguretat IP (IP Secure)", + "secure_tunnel_manual": "Configura manualment les claus de seguretat IP (IP Secure)" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "file_not_found": "No s'ha trobat el fitxer `.knxkeys` especificat a la ruta config/.storage/knx/", + "invalid_individual_address": "El valor no coincideix amb el patr\u00f3 d'adre\u00e7a KNX individual.\n'area.line.device'", + "invalid_ip_address": "Adre\u00e7a IPv4 inv\u00e0lida.", + "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", + "no_router_discovered": "No s'ha descobert cap encaminador ('router') KNXnet/IP a la xarxa.", + "no_tunnel_discovered": "No s'ha trobat cap servidor de tunelitzaci\u00f3 KNX a la xarxa." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Freq\u00fc\u00e8ncia m\u00e0xima", + "state_updater": "Actualitzador d'estat" + }, + "data_description": { + "rate_limit": "Telegrames de sortida m\u00e0xims per segon.\nUtilitza `0` per desactivar la limitaci\u00f3. Recomanat: 0 o, de 20 a 40", + "state_updater": "Configuraci\u00f3 predeterminadament per llegir els estats del bus KNX. Si est\u00e0 desactivat, Home Assistant no obtindr\u00e0 activament els estats del bus KNX. Les opcions d'entitat `sync_state` poden substituir-ho." + } + }, + "connection_type": { + "data": { + "connection_type": "Tipus de connexi\u00f3 KNX" + }, + "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament." + }, "init": { "data": { "connection_type": "Tipus de connexi\u00f3 KNX", @@ -105,8 +154,75 @@ "state_updater": "Configuraci\u00f3 predeterminadament per llegir els estats del bus KNX. Si est\u00e0 desactivat, Home Assistant no obtindr\u00e0 activament els estats del bus KNX. Les opcions d'entitat `sync_state` poden substituir-ho." } }, + "manual_tunnel": { + "data": { + "host": "Amfitri\u00f3", + "local_ip": "IP local de Home Assistant", + "port": "Port", + "route_back": "Encaminament de retorn / Mode NAT", + "tunneling_type": "Tipus de t\u00fanel KNX" + }, + "data_description": { + "host": "Adre\u00e7a IP del dispositiu de tunelitzaci\u00f3 KNX/IP.", + "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic.", + "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP.", + "route_back": "Activa-ho si el teun servidor de tunelitzaci\u00f3 KNXnet/IP est\u00e0 darrere una NAT. Nom\u00e9s s'aplica a connexions UDP." + }, + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del dispositiu de t\u00fanel." + }, + "options_init": { + "menu_options": { + "communication_settings": "Configuraci\u00f3 de la comunicaci\u00f3", + "connection_type": "Configura la interf\u00edcie KNX" + } + }, + "routing": { + "data": { + "individual_address": "Adre\u00e7a individual", + "local_ip": "IP local de Home Assistant", + "multicast_group": "Grup multidifusi\u00f3", + "multicast_port": "Port multidifusi\u00f3" + }, + "data_description": { + "individual_address": "Adre\u00e7a KNX per utilitzar amb Home Assistant, p. ex. `0.0.4`", + "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic." + }, + "description": "Configura les opcions d'encaminament." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nom del teu fitxer `.knxkeys` (inclosa l'extensi\u00f3)", + "knxkeys_password": "Contrasenya per desxifrar el fitxer `.knxkeys`." + }, + "data_description": { + "knxkeys_filename": "S'espera que el fitxer es trobi al teu directori de configuraci\u00f3 a `.storage/knx/`.\nA Home Assistant aix\u00f2 estaria a `/config/.storage/knx/`\nExemple: `el_meu_projecte.knxkeys`", + "knxkeys_password": "S'ha definit durant l'exportaci\u00f3 del fitxer des d'ETS." + }, + "description": "Introdueix la informaci\u00f3 del teu fitxer `.knxkeys`." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Contrasenya d'autenticaci\u00f3 del dispositiu", + "user_id": "ID d'usuari", + "user_password": "Contrasenya d'usuari" + }, + "data_description": { + "device_authentication": "S'estableix al panell 'IP' de la interf\u00edcie d'ETS.", + "user_id": "Sovint \u00e9s el n\u00famero del t\u00fanel +1. Per tant, 'T\u00fanel 2' tindria l'ID d'usuari '3'.", + "user_password": "Contrasenya per a la connexi\u00f3 t\u00fanel espec\u00edfica configurada al panell 'Propietats' del t\u00fanel a ETS." + }, + "description": "Introdueix la informaci\u00f3 de seguretat IP (IP Secure)." + }, + "secure_tunneling": { + "description": "Selecciona com vols configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilitza un fitxer `.knxkeys` que contingui les claus de seguretat IP (IP Secure)", + "secure_tunnel_manual": "Configura manualment les claus de seguretat IP (IP Secure)" + } + }, "tunnel": { "data": { + "gateway": "Connexi\u00f3 t\u00fanel KNX", "host": "Amfitri\u00f3", "port": "Port", "tunneling_type": "Tipus de t\u00fanel KNX" @@ -114,7 +230,8 @@ "data_description": { "host": "Adre\u00e7a IP del dispositiu de tunelitzaci\u00f3 KNX/IP.", "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP." - } + }, + "description": "Selecciona una passarel\u00b7la d'enlla\u00e7 de la llista." } } } diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 1daffa9c301c3d..2d624346e00801 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -9,20 +9,30 @@ "file_not_found": "Die angegebene `.knxkeys`-Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", "invalid_individual_address": "Wert ist keine g\u00fcltige physikalische Adresse. 'Bereich.Linie.Teilnehmer'", "invalid_ip_address": "Ung\u00fcltige IPv4 Adresse.", - "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei ist ung\u00fcltig." + "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei ist ung\u00fcltig.", + "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", + "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX-Verbindungstyp" + }, + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "Lokale IP von Home Assistant", "port": "Port", + "route_back": "Zur\u00fcckrouten / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" }, "data_description": { "host": "IP-Adresse der KNX/IP-Tunneling Schnittstelle.", "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden.", - "port": "Port der KNX/IP-Tunneling Schnittstelle." + "port": "Port der KNX/IP-Tunneling Schnittstelle.", + "route_back": "Aktiviere diese Option, wenn sich Ihr KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." }, "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." }, @@ -63,11 +73,25 @@ }, "description": "Bitte gib deine IP-Secure Informationen ein." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Ger\u00e4te-Authentifizierungscode", + "user_id": "Benutzer-ID", + "user_password": "Benutzer-Passwort" + }, + "data_description": { + "device_authentication": "Dies wird im Feld \"IP\" der Schnittstelle in ETS eingestellt.", + "user_id": "Dies ist oft die Tunnelnummer +1. \u201eTunnel 2\u201c h\u00e4tte also die Benutzer-ID \u201e3\u201c.", + "user_password": "Passwort f\u00fcr die spezifische Tunnelverbindung, die im Bereich \u201eEigenschaften\u201c des Tunnels in ETS festgelegt wurde." + }, + "description": "Bitte gib deine IP-Secure Informationen ein." + }, "secure_tunneling": { "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", "menu_options": { "secure_knxkeys": "Verwende eine `.knxkeys`-Datei, die IP-Secure-Schl\u00fcssel enth\u00e4lt", - "secure_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + "secure_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren", + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "file_not_found": "Die angegebene `.knxkeys`-Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", + "invalid_individual_address": "Wert ist keine g\u00fcltige physikalische Adresse. 'Bereich.Linie.Teilnehmer'", + "invalid_ip_address": "Ung\u00fcltige IPv4 Adresse.", + "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei ist ung\u00fcltig.", + "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", + "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Ratenlimit", + "state_updater": "Status-Updater" + }, + "data_description": { + "rate_limit": "Maximal ausgehende Telegramme pro Sekunde.\n `0`, um das Limit zu deaktivieren. Empfohlen: 0 oder 20 bis 40", + "state_updater": "Standardeinstellung f\u00fcr das Lesen von Zust\u00e4nden aus dem KNX-Bus. Wenn diese Option deaktiviert ist, wird der Home Assistant den Zustand der Entit\u00e4ten nicht aktiv vom KNX-Bus abrufen. Kann durch die Entity-Optionen `sync_state` au\u00dfer Kraft gesetzt werden." + } + }, + "connection_type": { + "data": { + "connection_type": "KNX-Verbindungstyp" + }, + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + }, "init": { "data": { "connection_type": "KNX-Verbindungstyp", @@ -105,8 +154,75 @@ "state_updater": "Standardeinstellung f\u00fcr das Lesen von Zust\u00e4nden aus dem KNX-Bus. Wenn diese Option deaktiviert ist, wird der Home Assistant den Zustand der Entit\u00e4ten nicht aktiv vom KNX-Bus abrufen. Kann durch die Entity-Optionen `sync_state` au\u00dfer Kraft gesetzt werden." } }, + "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "Lokale IP von Home Assistant", + "port": "Port", + "route_back": "Zur\u00fcckrouten / NAT-Modus", + "tunneling_type": "KNX Tunneling Typ" + }, + "data_description": { + "host": "IP-Adresse der KNX/IP-Tunneling Schnittstelle.", + "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden.", + "port": "Port der KNX/IP-Tunneling Schnittstelle.", + "route_back": "Aktiviere diese Option, wenn sich Ihr KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." + }, + "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." + }, + "options_init": { + "menu_options": { + "communication_settings": "Kommunikationseinstellungen", + "connection_type": "KNX-Schnittstelle konfigurieren" + } + }, + "routing": { + "data": { + "individual_address": "Physikalische Adresse", + "local_ip": "Lokale IP von Home Assistant", + "multicast_group": "Multicast-Gruppe", + "multicast_port": "Multicast-Port" + }, + "data_description": { + "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z. B. \u201e0.0.4\u201c.", + "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden." + }, + "description": "Bitte konfiguriere die Routing-Optionen." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Der Dateiname deiner `.knxkeys`-Datei (einschlie\u00dflich Erweiterung)", + "knxkeys_password": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei" + }, + "data_description": { + "knxkeys_filename": "Die Datei wird in deinem Konfigurationsverzeichnis unter `.storage/knx/` erwartet.\nIm Home Assistant OS w\u00e4re dies `/config/.storage/knx/`\nBeispiel: `my_project.knxkeys`", + "knxkeys_password": "Dies wurde beim Exportieren der Datei aus ETS gesetzt." + }, + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys`-Datei ein." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Ger\u00e4te-Authentifizierungscode", + "user_id": "Benutzer-ID", + "user_password": "Benutzer-Passwort" + }, + "data_description": { + "device_authentication": "Dies wird im Feld \"IP\" der Schnittstelle in ETS eingestellt.", + "user_id": "Dies ist oft die Tunnelnummer +1. \u201eTunnel 2\u201c h\u00e4tte also die Benutzer-ID \u201e3\u201c.", + "user_password": "Passwort f\u00fcr die spezifische Tunnelverbindung, die im Bereich \u201eEigenschaften\u201c des Tunnels in ETS festgelegt wurde." + }, + "description": "Bitte gib deine IP-Secure Informationen ein." + }, + "secure_tunneling": { + "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", + "menu_options": { + "secure_knxkeys": "Verwende eine `.knxkeys`-Datei, die IP-Secure-Schl\u00fcssel enth\u00e4lt", + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + } + }, "tunnel": { "data": { + "gateway": "KNX Tunnel Verbindung", "host": "Host", "port": "Port", "tunneling_type": "KNX Tunneling Typ" @@ -114,7 +230,8 @@ "data_description": { "host": "IP-Adresse der KNX/IP-Tunneling Schnittstelle.", "port": "Port der KNX/IP-Tunneling Schnittstelle." - } + }, + "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index c45c98b070a17f..920cd21b1cc6b2 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -60,6 +60,19 @@ }, "description": "Please enter the information for your `.knxkeys` file." }, + "secure_manual": { + "data": { + "device_authentication": "Device authentication password", + "user_id": "User ID", + "user_password": "User password" + }, + "data_description": { + "device_authentication": "This is set in the 'IP' panel of the interface in ETS.", + "user_id": "This is often tunnel number +1. So 'Tunnel 2' would have User-ID '3'.", + "user_password": "Password for the specific tunnel connection set in the 'Properties' panel of the tunnel in ETS." + }, + "description": "Please enter your IP secure information." + }, "secure_tunnel_manual": { "data": { "device_authentication": "Device authentication password", @@ -77,6 +90,7 @@ "description": "Select how you want to configure KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use a `.knxkeys` file containing IP secure keys", + "secure_manual": "Configure IP secure keys manually", "secure_tunnel_manual": "Configure IP secure keys manually" } }, @@ -85,6 +99,12 @@ "gateway": "KNX Tunnel Connection" }, "description": "Please select a gateway from the list." + }, + "type": { + "data": { + "connection_type": "KNX Connection Type" + }, + "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." } } }, @@ -115,6 +135,25 @@ }, "description": "Please enter the connection type we should use for your KNX connection. \n AUTOMATIC - The integration takes care of the connectivity to your KNX Bus by performing a gateway scan. \n TUNNELING - The integration will connect to your KNX bus via tunneling. \n ROUTING - The integration will connect to your KNX bus via routing." }, + "init": { + "data": { + "connection_type": "KNX Connection Type", + "individual_address": "Default individual address", + "local_ip": "Local IP of Home Assistant", + "multicast_group": "Multicast group", + "multicast_port": "Multicast port", + "rate_limit": "Rate limit", + "state_updater": "State updater" + }, + "data_description": { + "individual_address": "KNX address to be used by Home Assistant, e.g. `0.0.4`", + "local_ip": "Use `0.0.0.0` for auto-discovery.", + "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", + "multicast_port": "Used for routing and discovery. Default: `3671`", + "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40", + "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options." + } + }, "manual_tunnel": { "data": { "host": "Host", @@ -183,7 +222,14 @@ }, "tunnel": { "data": { - "gateway": "KNX Tunnel Connection" + "gateway": "KNX Tunnel Connection", + "host": "Host", + "port": "Port", + "tunneling_type": "KNX Tunneling Type" + }, + "data_description": { + "host": "IP address of the KNX/IP tunneling device.", + "port": "Port of the KNX/IP tunneling device." }, "description": "Please select a gateway from the list." } diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 19de37aaf56c01..df422c28208a35 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -9,20 +9,30 @@ "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", - "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta." + "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", + "no_router_discovered": "No se ha descubierto ning\u00fan router KNXnet/IP en la red.", + "no_tunnel_discovered": "No se pudo encontrar un servidor de t\u00fanel KNX en tu red." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipo de conexi\u00f3n KNX" + }, + "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad a tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a su tus KNX a trav\u00e9s del enrutamiento." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "IP local de Home Assistant", "port": "Puerto", + "route_back": "Ruta de regreso / modo NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { - "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", + "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico.", - "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." + "port": "Puerto del dispositivo de t\u00fanel KNX/IP.", + "route_back": "Habilitar si tu servidor de t\u00fanel IP/KNXnet est\u00e1 detr\u00e1s de NAT. Solo aplica para conexiones UDP." }, "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." }, @@ -63,11 +73,25 @@ }, "description": "Por favor, introduce tu informaci\u00f3n de IP segura." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Contrase\u00f1a de autenticaci\u00f3n del dispositivo", + "user_id": "ID de usuario", + "user_password": "Contrase\u00f1a de usuario" + }, + "data_description": { + "device_authentication": "Esto se configura en el panel 'IP' de la interfaz en ETS.", + "user_id": "Este suele ser el n\u00famero de t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda ID de usuario '3'.", + "user_password": "Contrase\u00f1a para la conexi\u00f3n de t\u00fanel espec\u00edfica establecida en el panel 'Propiedades' del t\u00fanel en ETS." + }, + "description": "Por favor, introduce tu informaci\u00f3n de IP segura." + }, "secure_tunneling": { "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", - "secure_manual": "Configurar claves seguras de IP manualmente" + "secure_manual": "Configurar claves seguras de IP manualmente", + "secure_tunnel_manual": "Configurar claves seguras de IP manualmente" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "No se pudo conectar", + "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", + "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", + "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", + "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", + "no_router_discovered": "No se ha descubierto ning\u00fan router KNXnet/IP en la red.", + "no_tunnel_discovered": "No se pudo encontrar un servidor de t\u00fanel KNX en tu red." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "L\u00edmite de tasa", + "state_updater": "Actualizador de estado" + }, + "data_description": { + "rate_limit": "N\u00famero m\u00e1ximo de telegramas salientes por segundo.\n`0` para deshabilitar el l\u00edmite. Recomendado: 0 o 20 a 40", + "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente los estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." + } + }, + "connection_type": { + "data": { + "connection_type": "Tipo de conexi\u00f3n KNX" + }, + "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad a tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a su tus KNX a trav\u00e9s del enrutamiento." + }, "init": { "data": { "connection_type": "Tipo de conexi\u00f3n KNX", @@ -105,16 +154,84 @@ "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente los estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." } }, - "tunnel": { + "manual_tunnel": { "data": { "host": "Host", + "local_ip": "IP local de Home Assistant", "port": "Puerto", + "route_back": "Ruta de regreso / modo NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { - "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", - "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." + "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico.", + "port": "Puerto del dispositivo de t\u00fanel KNX/IP.", + "route_back": "Habilitar si tu servidor de t\u00fanel IP/KNXnet est\u00e1 detr\u00e1s de NAT. Solo aplica para conexiones UDP." + }, + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." + }, + "options_init": { + "menu_options": { + "communication_settings": "Configuraci\u00f3n de comunicaci\u00f3n", + "connection_type": "Configurar interfaz KNX" } + }, + "routing": { + "data": { + "individual_address": "Direcci\u00f3n individual", + "local_ip": "IP local de Home Assistant", + "multicast_group": "Grupo multicast", + "multicast_port": "Puerto multicast" + }, + "data_description": { + "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico." + }, + "description": "Por favor, configura las opciones de enrutamiento." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "El nombre de tu archivo `.knxkeys` (incluyendo la extensi\u00f3n)", + "knxkeys_password": "Contrase\u00f1a para descifrar el archivo `.knxkeys`." + }, + "data_description": { + "knxkeys_filename": "Se espera que el archivo se encuentre en tu directorio de configuraci\u00f3n en `.storage/knx/`.\nEn Home Assistant OS ser\u00eda `/config/.storage/knx/`\nEjemplo: `mi_proyecto.knxkeys`", + "knxkeys_password": "Esto se configur\u00f3 al exportar el archivo desde ETS." + }, + "description": "Por favor, introduce la informaci\u00f3n de tu archivo `.knxkeys`." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Contrase\u00f1a de autenticaci\u00f3n del dispositivo", + "user_id": "ID de usuario", + "user_password": "Contrase\u00f1a de usuario" + }, + "data_description": { + "device_authentication": "Esto se configura en el panel 'IP' de la interfaz en ETS.", + "user_id": "Este suele ser el n\u00famero de t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda ID de usuario '3'.", + "user_password": "Contrase\u00f1a para la conexi\u00f3n de t\u00fanel espec\u00edfica establecida en el panel 'Propiedades' del t\u00fanel en ETS." + }, + "description": "Por favor, introduce tu informaci\u00f3n de IP segura." + }, + "secure_tunneling": { + "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", + "secure_tunnel_manual": "Configurar claves seguras de IP manualmente" + } + }, + "tunnel": { + "data": { + "gateway": "Conexi\u00f3n de t\u00fanel KNX", + "host": "Host", + "port": "Puerto", + "tunneling_type": "Tipo de t\u00fanel KNX" + }, + "data_description": { + "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", + "port": "Puerto del dispositivo de t\u00fanel KNX/IP." + }, + "description": "Por favor, selecciona una puerta de enlace de la lista." } } } diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index fe60f5404de48e..3ed4343802166b 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -9,20 +9,30 @@ "file_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", "invalid_individual_address": "V\u00e4\u00e4rtus ei \u00fchti KNX-i individuaalse aadressi mustriga.\n 'area.line.device'", "invalid_ip_address": "Kehtetu IPv4 aadress.", - "invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale." + "invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", + "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", + "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" + }, + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "Home Assistanti kohalik IP aadress", "port": "Port", + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", "tunneling_type": "KNX tunneli t\u00fc\u00fcp" }, "data_description": { "host": "KNX/IP tunneldusseadme IP-aadress.", "local_ip": "Automaatse avastamise kasutamiseks j\u00e4ta t\u00fchjaks.", - "port": "KNX/IP-tunneldusseadme port." + "port": "KNX/IP-tunneldusseadme port.", + "route_back": "Luba, kui KNXneti/IP tunneldusserver on NAT-i taga. Kehtib ainult UDP-\u00fchenduste puhul." }, "description": "Sisesta tunneldamisseadme \u00fchenduse teave." }, @@ -63,11 +73,25 @@ }, "description": "Sisesta IP Secure teave." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Seadme autentimise parool", + "user_id": "Kasutaja ID", + "user_password": "Kasutaja salas\u00f5na" + }, + "data_description": { + "device_authentication": "See m\u00e4\u00e4ratakse ETSi liidese IP-paneelil.", + "user_id": "See on sageli tunneli number +1. Nii et tunnel 2 oleks kasutaja ID-ga 3.", + "user_password": "Konkreetse tunneli\u00fchenduse parool, mis on m\u00e4\u00e4ratud ETS-i tunneli paneelil \u201eAtribuudid\u201d." + }, + "description": "Sisesta oma IP secure teave." + }, "secure_tunneling": { "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", "menu_options": { "secure_knxkeys": "Kasuta knxkeys fail, mis sisaldab IP Secure teavet.", - "secure_manual": "IP Secure v\u00f5tmete k\u00e4sitsi seadistamine" + "secure_manual": "IP Secure v\u00f5tmete k\u00e4sitsi seadistamine", + "secure_tunnel_manual": "IP Secure v\u00f5tmete k\u00e4sitsi seadistamine" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "file_not_found": "M\u00e4\u00e4ratud kirjet '.knxkeys' ei leitud asukohast config/.storage/knx/", + "invalid_individual_address": "V\u00e4\u00e4rtuse mall ei vasta KNX seadme \u00fcksuse aadressile.\n'area.line.device'", + "invalid_ip_address": "Vigane IPv4 aadress", + "invalid_signature": "'.knxkeys' kirje dekr\u00fcptimisv\u00f5ti on vale.", + "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", + "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Teavituste m\u00e4\u00e4r", + "state_updater": "Oleku uuendaja" + }, + "data_description": { + "rate_limit": "Maksimaalne v\u00e4ljaminevate telegrammide arv sekundis. '0 piirangu eemaldamiseks. Soovitatav: 20 kuni 40", + "state_updater": "M\u00e4\u00e4ra KNX siini olekute lugemise vaikev\u00e4\u00e4rtused. Kui see on keelatud, ei too Home Assistant aktiivselt olemi olekuid KNX siinilt. Saab alistada olemivalikute s\u00fcnkroonimise_olekuga." + } + }, + "connection_type": { + "data": { + "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" + }, + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + }, "init": { "data": { "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp", @@ -105,8 +154,75 @@ "state_updater": "M\u00e4\u00e4ra KNX siini olekute lugemise vaikev\u00e4\u00e4rtused. Kui see on keelatud, ei too Home Assistant aktiivselt olemi olekuid KNX siinilt. Saab alistada olemivalikute s\u00fcnkroonimise_olekuga." } }, + "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "Home Assistanti kohtv\u00f5rgu IP", + "port": "Port", + "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", + "tunneling_type": "KNX tunneli t\u00fc\u00fcp" + }, + "data_description": { + "host": "KNX/IP tunneldusseadme IP aadress.", + "local_ip": "Automaatseks tuvastamiseks j\u00e4ta t\u00fchjaks.", + "port": "KNX/IP tunneldusseadme port.", + "route_back": "Luba kui KNXnet/IP server on NAT-i taga. Kehtib ainult UDP \u00fchendustele." + }, + "description": "Sisesta tunnel\u00fchenduse parameetrid." + }, + "options_init": { + "menu_options": { + "communication_settings": "\u00dchenduse seaded", + "connection_type": "Seadista KNX liides" + } + }, + "routing": { + "data": { + "individual_address": "\u00dcksuse aadress", + "local_ip": "Home Assistati kohtv\u00f5rgu IP aadress", + "multicast_group": "Multicasti grupp", + "multicast_port": "Multicasti port" + }, + "data_description": { + "individual_address": "Home Assistantis kasutatav KNX aadress, n\u00e4iteks '0.0.4''", + "local_ip": "Automaatseks tuvastamiseks j\u00e4ta t\u00fchjaks." + }, + "description": "Seadista marsruutimine" + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "'.knxkeys' kirje nimi (koos laiendiga)", + "knxkeys_password": "Kirje '.knxkeys' dekr\u00fcptimise v\u00f5ti" + }, + "data_description": { + "knxkeys_filename": "See kirje peaks asuma seadete kaustas '.storage/knx/'.\nHome Assistant OS puhul oleks see 'config/.storage/knx/'\nN\u00e4iteks: 'my_project.knxkeys'", + "knxkeys_password": "See saadi kirje eksportisel ETS-ist." + }, + "description": "Sisesta oma '.knxkeys' kirje teave" + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Seadme tuvastamise salas\u00f5na", + "user_id": "Kasutaja ID", + "user_password": "Kasutaja salas\u00f5na" + }, + "data_description": { + "device_authentication": "Seda saab seada ETS liidese 'IP' paneelil", + "user_id": "See on tavaliselt tunneli number+1. Seega 'Tunnel 2' on kasutaja ID-ga '3'.", + "user_password": "Konkreetse tunneli\u00fchenduse parool, mis on m\u00e4\u00e4ratud ETS-i tunneli paneelil \u201eAtribuudid\u201d." + }, + "description": "Sisesta IP secure teave." + }, + "secure_tunneling": { + "description": "Vali kuidas seadistada KNX/IP Secure", + "menu_options": { + "secure_knxkeys": "Kasuta IP secure jaoks kirjet '.knxkeys'", + "secure_tunnel_manual": "Seadista IP secure v\u00f5tmed k\u00e4sitsi" + } + }, "tunnel": { "data": { + "gateway": "KNX tunnel\u00fchendus", "host": "Host", "port": "Port", "tunneling_type": "KNX tunneli t\u00fc\u00fcp" @@ -114,7 +230,8 @@ "data_description": { "host": "KNX/IP tunneldusseadme IP-aadress.", "port": "KNX/IP-tunneldusseadme port." - } + }, + "description": "Vali nimekirjast l\u00fc\u00fcs" } } } diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 92411b58312bf7..1aac9fb1b89efb 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -9,20 +9,30 @@ "file_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "invalid_individual_address": "Az \u00e9rt\u00e9k nem felel meg a KNX egyedi c\u00edm mint\u00e1j\u00e1nak.\n'area.line.device'", "invalid_ip_address": "\u00c9rv\u00e9nytelen IPv4-c\u00edm.", - "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen." + "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", + "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" + }, + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + }, "manual_tunnel": { "data": { "host": "C\u00edm", "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", "port": "Port", + "route_back": "Vissza\u00fat / NAT m\u00f3d", "tunneling_type": "KNX alag\u00fat t\u00edpusa" }, "data_description": { "host": "A KNX/IP tunnel eszk\u00f6z IP-c\u00edme.", "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen.", - "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma." + "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", + "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." }, "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." }, @@ -63,11 +73,25 @@ }, "description": "K\u00e9rem, adja meg az IP secure adatokat." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Eszk\u00f6z hiteles\u00edt\u00e9si jelsz\u00f3", + "user_id": "Felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3", + "user_password": "Felhaszn\u00e1l\u00f3i jelsz\u00f3" + }, + "data_description": { + "device_authentication": "Ezt az ETS-ben az interf\u00e9sz \"IP\" panelj\u00e9n kell be\u00e1ll\u00edtani.", + "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", + "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." + }, + "description": "K\u00e9rem, adja meg az IP secure adatokat." + }, "secure_tunneling": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", "menu_options": { "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", - "secure_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa" + "secure_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa", + "secure_tunnel_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "file_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", + "invalid_individual_address": "Az \u00e9rt\u00e9k nem felel meg a KNX egyedi c\u00edm mint\u00e1j\u00e1nak.\n'area.line.device'", + "invalid_ip_address": "\u00c9rv\u00e9nytelen IPv4-c\u00edm.", + "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", + "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Lek\u00e9r\u00e9si korl\u00e1toz\u00e1s", + "state_updater": "\u00c1llapot friss\u00edt\u0151" + }, + "data_description": { + "rate_limit": "Maxim\u00e1lisan kimen\u0151 \u00fczenet m\u00e1sodpercenk\u00e9nt. 0 a kikapcsol\u00e1shoz.\nAj\u00e1nlott: 0, vagy 20 \u00e9s 40 k\u00f6z\u00f6tt", + "state_updater": "Alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1s a KNX busz \u00e1llapotainak olvas\u00e1s\u00e1hoz. Ha le va tiltva, Home Assistant nem fog akt\u00edvan lek\u00e9rdezni egys\u00e9g\u00e1llapotokat a KNX buszr\u00f3l. Fel\u00fclb\u00edr\u00e1lhat\u00f3 a `sync_state` entit\u00e1s opci\u00f3kkal." + } + }, + "connection_type": { + "data": { + "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" + }, + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + }, "init": { "data": { "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa", @@ -105,8 +154,75 @@ "state_updater": "Alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1s a KNX busz \u00e1llapotainak olvas\u00e1s\u00e1hoz. Ha le va tiltva, Home Assistant nem fog akt\u00edvan lek\u00e9rdezni egys\u00e9g\u00e1llapotokat a KNX buszr\u00f3l. Fel\u00fclb\u00edr\u00e1lhat\u00f3 a `sync_state` entit\u00e1s opci\u00f3kkal." } }, + "manual_tunnel": { + "data": { + "host": "C\u00edm", + "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", + "port": "Port", + "route_back": "Vissza\u00fat / NAT m\u00f3d", + "tunneling_type": "KNX alag\u00fat t\u00edpusa" + }, + "data_description": { + "host": "A KNX/IP tunnel eszk\u00f6z IP-c\u00edme.", + "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen.", + "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", + "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." + }, + "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." + }, + "options_init": { + "menu_options": { + "communication_settings": "Kommunik\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sok", + "connection_type": "KNX interf\u00e9sz konfigur\u00e1l\u00e1sa" + } + }, + "routing": { + "data": { + "individual_address": "Egy\u00e9ni c\u00edm", + "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", + "multicast_group": "Multicast csoport", + "multicast_port": "Multicast portsz\u00e1m" + }, + "data_description": { + "individual_address": "A Home Assistant \u00e1ltal haszn\u00e1land\u00f3 KNX-c\u00edm, pl. \"0.0.4\".", + "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen." + }, + "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "A '.knxkeys' f\u00e1jl teljes neve (kiterjeszt\u00e9ssel)", + "knxkeys_password": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez sz\u00fcks\u00e9ges jelsz\u00f3" + }, + "data_description": { + "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", + "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." + }, + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Eszk\u00f6z hiteles\u00edt\u00e9si jelsz\u00f3", + "user_id": "Felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3", + "user_password": "Felhaszn\u00e1l\u00f3i jelsz\u00f3" + }, + "data_description": { + "device_authentication": "Ezt az ETS-ben az interf\u00e9sz \"IP\" panelj\u00e9n kell be\u00e1ll\u00edtani.", + "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", + "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." + }, + "description": "K\u00e9rem, adja meg az IP secure adatokat." + }, + "secure_tunneling": { + "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", + "menu_options": { + "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", + "secure_tunnel_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa" + } + }, "tunnel": { "data": { + "gateway": "KNX alag\u00fat (tunnel) kapcsolat", "host": "C\u00edm", "port": "Port", "tunneling_type": "KNX alag\u00fat t\u00edpusa" @@ -114,7 +230,8 @@ "data_description": { "host": "A KNX/IP tunnel eszk\u00f6z IP-c\u00edme.", "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma." - } + }, + "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." } } } diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index bbf9a1b78629ba..6b4977e543a53d 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -9,20 +9,30 @@ "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "invalid_individual_address": "Nilai tidak cocok dengan pola untuk alamat individual KNX.\n'area.line.device'", "invalid_ip_address": "Alamat IPv4 tidak valid", - "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah." + "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", + "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda." }, "step": { + "connection_type": { + "data": { + "connection_type": "Jenis Koneksi KNX" + }, + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "IP lokal Home Assistant", "port": "Port", + "route_back": "Dirutekan kembali/Mode NAT", "tunneling_type": "Jenis Tunnel KNX" }, "data_description": { "host": "Alamat IP perangkat tunneling KNX/IP.", "local_ip": "Kosongkan untuk menggunakan penemuan otomatis.", - "port": "Port perangkat tunneling KNX/IP." + "port": "Port perangkat tunneling KNX/IP.", + "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." }, "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." }, @@ -63,11 +73,25 @@ }, "description": "Masukkan informasi IP aman Anda." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Kata sandi autentikasi perangkat", + "user_id": "ID pengguna", + "user_password": "Kata sandi pengguna" + }, + "data_description": { + "device_authentication": "Ini diatur dalam panel 'IP' dalam antarmuka di ETS.", + "user_id": "Ini sering kali merupakan tunnel nomor +1. Jadi 'Tunnel 2' akan memiliki User-ID '3'.", + "user_password": "Kata sandi untuk koneksi tunnel tertentu yang diatur di panel 'Properties' tunnel di ETS." + }, + "description": "Masukkan informasi IP aman Anda." + }, "secure_tunneling": { "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", - "secure_manual": "Konfigurasikan kunci aman IP secara manual" + "secure_manual": "Konfigurasikan kunci aman IP secara manual", + "secure_tunnel_manual": "Konfigurasikan kunci aman IP secara manual" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "Gagal terhubung", + "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", + "invalid_individual_address": "Nilai tidak cocok dengan pola untuk alamat individual KNX.\n'area.line.device'", + "invalid_ip_address": "Alamat IPv4 tidak valid", + "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", + "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Batas data", + "state_updater": "Pembaruan status" + }, + "data_description": { + "rate_limit": "Telegram keluar maksimum per detik. `0` untuk menonaktifkan batas. Direkomendasikan: 0 atau 20 hingga 40", + "state_updater": "Menyetel default untuk status pembacaan KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status entitas dari KNX Bus. Hal ini bisa ditimpa dengan opsi entitas `sync_state`." + } + }, + "connection_type": { + "data": { + "connection_type": "Jenis Koneksi KNX" + }, + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + }, "init": { "data": { "connection_type": "Jenis Koneksi KNX", @@ -105,8 +154,75 @@ "state_updater": "Menyetel default untuk status pembacaan KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status entitas dari KNX Bus. Hal ini bisa ditimpa dengan opsi entitas `sync_state`." } }, + "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "IP lokal Home Assistant", + "port": "Port", + "route_back": "Dirutekan kembali/Mode NAT", + "tunneling_type": "Jenis Tunnel KNX" + }, + "data_description": { + "host": "Alamat IP perangkat tunneling KNX/IP.", + "local_ip": "Kosongkan untuk menggunakan penemuan otomatis.", + "port": "Port perangkat tunneling KNX/IP.", + "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." + }, + "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." + }, + "options_init": { + "menu_options": { + "communication_settings": "Pengaturan komunikasi", + "connection_type": "Konfigurasikan antarmuka KNX" + } + }, + "routing": { + "data": { + "individual_address": "Alamat individual", + "local_ip": "IP lokal Home Assistant", + "multicast_group": "Grup multicast", + "multicast_port": "Port multicast" + }, + "data_description": { + "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", + "local_ip": "Kosongkan untuk menggunakan penemuan otomatis." + }, + "description": "Konfigurasikan opsi routing." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nama file `.knxkeys` Anda (termasuk ekstensi)", + "knxkeys_password": "Kata sandi untuk mendekripsi file `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "File diharapkan dapat ditemukan di direktori konfigurasi Anda di `.storage/knx/`.\nDi Home Assistant OS ini akan menjadi `/config/.storage/knx/`\nContoh: `proyek_saya.knxkeys`", + "knxkeys_password": "Ini disetel saat mengekspor file dari ETS." + }, + "description": "Masukkan informasi untuk file `.knxkeys` Anda." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Kata sandi autentikasi perangkat", + "user_id": "ID pengguna", + "user_password": "Kata sandi pengguna" + }, + "data_description": { + "device_authentication": "Ini diatur dalam panel 'IP' dalam antarmuka di ETS.", + "user_id": "Ini sering kali merupakan tunnel nomor +1. Jadi 'Tunnel 2' akan memiliki User-ID '3'.", + "user_password": "Kata sandi untuk koneksi tunnel tertentu yang diatur di panel 'Properties' tunnel di ETS." + }, + "description": "Masukkan informasi IP aman Anda." + }, + "secure_tunneling": { + "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", + "secure_tunnel_manual": "Konfigurasikan kunci aman IP secara manual" + } + }, "tunnel": { "data": { + "gateway": "Koneksi Tunnel KNX", "host": "Host", "port": "Port", "tunneling_type": "Jenis Tunnel KNX" @@ -114,7 +230,8 @@ "data_description": { "host": "Alamat IP perangkat tunneling KNX/IP.", "port": "Port perangkat tunneling KNX/IP." - } + }, + "description": "Pilih gateway dari daftar." } } } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index 8c2c58ad2d519f..4a7bc92652daac 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -9,20 +9,30 @@ "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "invalid_individual_address": "Il valore non corrisponde al modello per l'indirizzo individuale KNX. 'area.line.device'", "invalid_ip_address": "Indirizzo IPv4 non valido.", - "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata." + "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", + "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipo di connessione KNX" + }, + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + }, "manual_tunnel": { "data": { "host": "Host", "local_ip": "IP locale di Home Assistant", "port": "Porta", + "route_back": "Modalit\u00e0 Route Back / NAT", "tunneling_type": "Tipo tunnel KNX" }, "data_description": { "host": "Indirizzo IP del dispositivo di tunneling KNX/IP.", "local_ip": "Lascia vuoto per usare il rilevamento automatico.", - "port": "Porta del dispositivo di tunneling KNX/IP." + "port": "Porta del dispositivo di tunneling KNX/IP.", + "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." }, "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." }, @@ -35,7 +45,7 @@ }, "data_description": { "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", - "local_ip": "Lasciare vuoto per usare il rilevamento automatico." + "local_ip": "Lascia vuoto per usare il rilevamento automatico." }, "description": "Configura le opzioni di instradamento." }, @@ -45,7 +55,7 @@ "knxkeys_password": "La password per decifrare il file `.knxkeys`" }, "data_description": { - "knxkeys_filename": "Il file dovrebbe essere trovato nella tua cartella di configurazione in `.storage/knx/`.\n Nel Sistema Operativo di Home Assistant questo sarebbe `/config/.storage/knx/`\n Esempio: `mio_progetto.knxkeys`", + "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." }, "description": "Inserisci le informazioni per il tuo file `.knxkeys`." @@ -63,11 +73,25 @@ }, "description": "Inserisci le tue informazioni di sicurezza IP." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Password di autenticazione del dispositivo", + "user_id": "ID utente", + "user_password": "Password utente" + }, + "data_description": { + "device_authentication": "Questo \u00e8 impostato nel pannello 'IP' dell'interfaccia in ETS.", + "user_id": "Questo \u00e8 spesso il tunnel numero +1. Quindi \"Tunnel 2\" avrebbe l'ID utente \"3\".", + "user_password": "Password per la connessione specifica del tunnel impostata nel pannello 'Propriet\u00e0' del tunnel in ETS." + }, + "description": "Inserisci le tue informazioni di sicurezza IP." + }, "secure_tunneling": { "description": "Seleziona come vuoi configurare KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Utilizza un file `.knxkeys` contenente chiavi di sicurezza IP", - "secure_manual": "Configura manualmente le chiavi di sicurezza IP" + "secure_manual": "Configura manualmente le chiavi di sicurezza IP", + "secure_tunnel_manual": "Configura manualmente le chiavi di sicurezza IP" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "Impossibile connettersi", + "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", + "invalid_individual_address": "Il valore non corrisponde al modello per l'indirizzo individuale KNX. 'area.line.device'", + "invalid_ip_address": "Indirizzo IPv4 non valido.", + "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", + "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Limite di velocit\u00e0", + "state_updater": "Aggiornatore di stato" + }, + "data_description": { + "rate_limit": "Numero massimo di telegrammi in uscita al secondo.\n'0' per disabilitare il limite. Consigliato: 0 o da 20 a 40", + "state_updater": "Impostazione predefinita per la lettura degli stati dal bus KNX. Quando disabilitato, Home Assistant non recuperer\u00e0 attivamente gli stati delle entit\u00e0 dal bus KNX. Pu\u00f2 essere sovrascritto dalle opzioni dell'entit\u00e0 `sync_state`." + } + }, + "connection_type": { + "data": { + "connection_type": "Tipo di connessione KNX" + }, + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + }, "init": { "data": { "connection_type": "Tipo di connessione KNX", @@ -105,8 +154,75 @@ "state_updater": "Impostazione predefinita per la lettura degli stati dal bus KNX. Se disabilitata Home Assistant non recuperer\u00e0 attivamente gli stati delle entit\u00e0 dal bus KNX. Pu\u00f2 essere sovrascritta dalle opzioni dell'entit\u00e0 `sync_state`." } }, + "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "IP locale di Home Assistant", + "port": "Porta", + "route_back": "Modalit\u00e0 Route Back / NAT", + "tunneling_type": "Tipo tunnel KNX" + }, + "data_description": { + "host": "Indirizzo IP del dispositivo di tunneling KNX/IP.", + "local_ip": "Lascia vuoto per usare il rilevamento automatico.", + "port": "Porta del dispositivo di tunneling KNX/IP.", + "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." + }, + "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." + }, + "options_init": { + "menu_options": { + "communication_settings": "Impostazioni di comunicazione", + "connection_type": "Configura interfaccia KNX" + } + }, + "routing": { + "data": { + "individual_address": "Indirizzo individuale", + "local_ip": "IP locale di Home Assistant", + "multicast_group": "Gruppo multicast", + "multicast_port": "Porta multicast" + }, + "data_description": { + "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", + "local_ip": "Lascia vuoto per usare il rilevamento automatico." + }, + "description": "Configura le opzioni di instradamento." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Il nome del file `.knxkeys` (inclusa l'estensione)", + "knxkeys_password": "La password per decifrare il file `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", + "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." + }, + "description": "Inserisci le informazioni per il tuo file `.knxkeys`." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Password di autenticazione del dispositivo", + "user_id": "ID utente", + "user_password": "Password utente" + }, + "data_description": { + "device_authentication": "Questo \u00e8 impostato nel pannello 'IP' dell'interfaccia in ETS.", + "user_id": "Questo \u00e8 spesso il tunnel numero +1. Quindi \"Tunnel 2\" avrebbe l'ID utente \"3\".", + "user_password": "Password per la connessione specifica del tunnel impostata nel pannello 'Propriet\u00e0' del tunnel in ETS." + }, + "description": "Inserisci le tue informazioni di sicurezza IP." + }, + "secure_tunneling": { + "description": "Seleziona come vuoi configurare KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilizza un file `.knxkeys` contenente chiavi di sicurezza IP", + "secure_tunnel_manual": "Configura manualmente le chiavi di sicurezza IP" + } + }, "tunnel": { "data": { + "gateway": "Connessione tunnel KNX", "host": "Host", "port": "Porta", "tunneling_type": "Tipo tunnel KNX" @@ -114,7 +230,8 @@ "data_description": { "host": "Indirizzo IP del dispositivo di tunneling KNX/IP.", "port": "Porta del dispositivo di tunneling KNX/IP." - } + }, + "description": "Seleziona un gateway dall'elenco." } } } diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index dcdf057493bb6f..bae7dd2f82ca57 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -9,20 +9,30 @@ "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "invalid_individual_address": "O valor n\u00e3o corresponde ao padr\u00e3o do endere\u00e7o individual KNX.\n '\u00e1rea.linha.dispositivo'", "invalid_ip_address": "Endere\u00e7o IPv4 inv\u00e1lido.", - "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada." + "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", + "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede." }, "step": { + "connection_type": { + "data": { + "connection_type": "Tipo de conex\u00e3o KNX" + }, + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + }, "manual_tunnel": { "data": { "host": "Nome do host", "local_ip": "IP local do Home Assistant", "port": "Porta", + "route_back": "Rota de volta / modo NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { "host": "Endere\u00e7o IP do dispositivo de tunelamento KNX/IP.", "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica.", - "port": "Porta do dispositivo de tunelamento KNX/IP." + "port": "Porta do dispositivo de tunelamento KNX/IP.", + "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." }, "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." }, @@ -63,11 +73,25 @@ }, "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Senha de autentica\u00e7\u00e3o do dispositivo", + "user_id": "ID do usu\u00e1rio", + "user_password": "Senha do usu\u00e1rio" + }, + "data_description": { + "device_authentication": "Isso \u00e9 definido no painel 'IP' da interface no ETS.", + "user_id": "Isso geralmente \u00e9 o n\u00famero do t\u00fanel +1. Portanto, 'T\u00fanel 2' teria o ID de usu\u00e1rio '3'.", + "user_password": "Senha para a conex\u00e3o de t\u00fanel espec\u00edfica definida no painel 'Propriedades' do t\u00fanel no ETS." + }, + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + }, "secure_tunneling": { "description": "Selecione como deseja configurar o KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves seguras de IP", - "secure_manual": "Configurar manualmente as chaves de seguran\u00e7a IP" + "secure_manual": "Configurar manualmente as chaves de seguran\u00e7a IP", + "secure_tunnel_manual": "Configurar chaves seguras de IP manualmente" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "Falhou ao conectar", + "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", + "invalid_individual_address": "O valor n\u00e3o corresponde ao padr\u00e3o do endere\u00e7o individual KNX.\n'\u00e1rea.linha.dispositivo'", + "invalid_ip_address": "Endere\u00e7o IPv4 inv\u00e1lido.", + "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", + "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Taxa limite", + "state_updater": "Atualizador de estado" + }, + "data_description": { + "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo.\n `0` para desabilitar o limite. Recomendado: 0 ou 20 a 40", + "state_updater": "Defina o padr\u00e3o para estados de leitura do barramento KNX. Quando desativado, o Home Assistant n\u00e3o recuperar\u00e1 ativamente os estados de entidade do barramento KNX. Pode ser substitu\u00eddo pelas op\u00e7\u00f5es de entidade `sync_state`." + } + }, + "connection_type": { + "data": { + "connection_type": "Tipo de conex\u00e3o KNX" + }, + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + }, "init": { "data": { "connection_type": "Tipo de conex\u00e3o KNX", @@ -105,8 +154,75 @@ "state_updater": "Defina o padr\u00e3o para estados de leitura do barramento KNX. Quando desativado, o Home Assistant n\u00e3o recuperar\u00e1 ativamente os estados de entidade do barramento KNX. Pode ser substitu\u00eddo pelas op\u00e7\u00f5es de entidade `sync_state`." } }, + "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "IP local do Home Assistant", + "port": "Porta", + "route_back": "Rota de volta / modo NAT", + "tunneling_type": "Tipo de t\u00fanel KNX" + }, + "data_description": { + "host": "Endere\u00e7o IP do dispositivo de encapsulamento KNX/IP.", + "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica.", + "port": "Porta do dispositivo de encapsulamento KNX/IP.", + "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." + }, + "description": "Insira as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de encapsulamento." + }, + "options_init": { + "menu_options": { + "communication_settings": "Configura\u00e7\u00f5es de comunica\u00e7\u00e3o", + "connection_type": "Configurar interface KNX" + } + }, + "routing": { + "data": { + "individual_address": "Endere\u00e7o individual", + "local_ip": "IP local do Home Assistant", + "multicast_group": "Grupo multicast", + "multicast_port": "Porta multicast" + }, + "data_description": { + "individual_address": "Endere\u00e7o KNX a ser utilizado pelo Home Assistant, por ex. `0.0.4`", + "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica." + }, + "description": "Por favor, configure as op\u00e7\u00f5es de roteamento." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "O nome do seu arquivo `.knxkeys` (incluindo extens\u00e3o)", + "knxkeys_password": "A senha para descriptografar o arquivo `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Espera-se que o arquivo seja encontrado em seu diret\u00f3rio de configura\u00e7\u00e3o em `.storage/knx/`.\nNo sistema operacional Home Assistant seria `/config/.storage/knx/`\nExemplo: `my_project.knxkeys`", + "knxkeys_password": "Isso foi definido ao exportar o arquivo do ETS." + }, + "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Senha de autentica\u00e7\u00e3o do dispositivo", + "user_id": "ID do usu\u00e1rio", + "user_password": "Senha do usu\u00e1rio" + }, + "data_description": { + "device_authentication": "Isso \u00e9 definido no painel 'IP' da interface no ETS.", + "user_id": "Isso geralmente \u00e9 o n\u00famero do t\u00fanel +1. Portanto, 'T\u00fanel 2' teria o ID de usu\u00e1rio '3'.", + "user_password": "Senha para a conex\u00e3o de t\u00fanel espec\u00edfica definida no painel 'Propriedades' do t\u00fanel no ETS." + }, + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + }, + "secure_tunneling": { + "description": "Selecione como deseja configurar o KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves seguras de IP", + "secure_tunnel_manual": "Configurar chaves seguras de IP manualmente" + } + }, "tunnel": { "data": { + "gateway": "Conex\u00e3o do KNX Tunnel", "host": "Nome do host", "port": "Porta", "tunneling_type": "Tipo de t\u00fanel KNX" @@ -114,7 +230,8 @@ "data_description": { "host": "Endere\u00e7o IP do dispositivo de tunelamento KNX/IP.", "port": "Porta do dispositivo de tunelamento KNX/IP." - } + }, + "description": "Selecione um gateway na lista." } } } diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index b348f38701d9a3..90f98a31187dca 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -9,20 +9,30 @@ "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "invalid_individual_address": "\u6578\u503c\u8207 KNX \u500b\u5225\u4f4d\u5740\u4e0d\u76f8\u7b26\u3002\n'area.line.device'", "invalid_ip_address": "IPv4 \u4f4d\u5740\u7121\u6548\u3002", - "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002" + "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", + "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002" }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX \u9023\u7dda\u985e\u578b" + }, + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + }, "manual_tunnel": { "data": { "host": "\u4e3b\u6a5f\u7aef", "local_ip": "Home Assistant \u672c\u5730\u7aef IP", "port": "\u901a\u8a0a\u57e0", + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, "data_description": { "host": "KNX/IP \u901a\u9053\u88dd\u7f6e IP \u4f4d\u5740\u3002", "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002", - "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002" + "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", + "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" }, "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" }, @@ -63,11 +73,25 @@ }, "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "\u88dd\u7f6e\u8a8d\u8b49\u5bc6\u78bc", + "user_id": "\u4f7f\u7528\u8005 ID", + "user_password": "\u4f7f\u7528\u8005\u5bc6\u78bc" + }, + "data_description": { + "device_authentication": "\u65bc EST \u4ecb\u9762\u4e2d 'IP' \u9762\u677f\u9032\u884c\u8a2d\u5b9a\u3002", + "user_id": "\u901a\u5e38\u70ba\u901a\u9053\u6578 +1\u3002\u56e0\u6b64 'Tunnel 2' \u5c07\u5177\u6709\u4f7f\u7528\u8005 ID '3'\u3002", + "user_password": "\u65bc ETS \u901a\u9053 'Properties' \u9762\u677f\u53ef\u8a2d\u5b9a\u6307\u5b9a\u901a\u9053\u9023\u7dda\u5bc6\u78bc\u3002" + }, + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + }, "secure_tunneling": { "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", "menu_options": { "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", - "secure_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000" + "secure_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000", + "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", + "invalid_individual_address": "\u6578\u503c\u8207 KNX \u500b\u5225\u4f4d\u5740\u4e0d\u76f8\u7b26\u3002\n'area.line.device'", + "invalid_ip_address": "IPv4 \u4f4d\u5740\u7121\u6548\u3002", + "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", + "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002" + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "\u983b\u7387\u9650\u5236", + "state_updater": "\u72c0\u614b\u66f4\u65b0\u5668" + }, + "data_description": { + "rate_limit": "\u6bcf\u79d2\u6700\u5927 Telegram \u767c\u9001\u91cf\u3002\n`0` \u70ba\u95dc\u9589\u9650\u5236\u3001\u5efa\u8b70\uff1a0 \u6216 20 \u81f3 40", + "state_updater": "\u8a2d\u5b9a\u9810\u8a2d KNX Bus \u8b80\u53d6\u72c0\u614b\u3002\u7576\u95dc\u9589\u6642\u3001Home Assistant \u5c07\u4e0d\u6703\u4e3b\u52d5\u5f9e KNX Bus \u7372\u53d6\u5be6\u9ad4\u72c0\u614b\uff0c\u53ef\u88ab`sync_state` \u5be6\u9ad4\u9078\u9805\u8986\u84cb\u3002" + } + }, + "connection_type": { + "data": { + "connection_type": "KNX \u9023\u7dda\u985e\u578b" + }, + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + }, "init": { "data": { "connection_type": "KNX \u9023\u7dda\u985e\u5225", @@ -94,7 +143,7 @@ "multicast_group": "Multicast \u7fa4\u7d44", "multicast_port": "Multicast \u901a\u8a0a\u57e0", "rate_limit": "\u983b\u7387\u9650\u5236", - "state_updater": "\u88dd\u614b\u66f4\u65b0\u5668" + "state_updater": "\u72c0\u614b\u66f4\u65b0\u5668" }, "data_description": { "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", @@ -105,8 +154,75 @@ "state_updater": "\u8a2d\u5b9a\u9810\u8a2d KNX Bus \u8b80\u53d6\u72c0\u614b\u3002\u7576\u95dc\u9589\u6642\u3001Home Assistant \u5c07\u4e0d\u6703\u4e3b\u52d5\u5f9e KNX Bus \u7372\u53d6\u5be6\u9ad4\u72c0\u614b\uff0c\u53ef\u88ab`sync_state` \u5be6\u9ad4\u9078\u9805\u8986\u84cb\u3002" } }, + "manual_tunnel": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "local_ip": "Home Assistant \u672c\u5730\u7aef IP", + "port": "\u901a\u8a0a\u57e0", + "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", + "tunneling_type": "KNX \u901a\u9053\u985e\u5225" + }, + "data_description": { + "host": "KNX/IP \u901a\u9053\u88dd\u7f6e IP \u4f4d\u5740\u3002", + "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002", + "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", + "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" + }, + "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" + }, + "options_init": { + "menu_options": { + "communication_settings": "\u901a\u8a0a\u8a2d\u5b9a", + "connection_type": "\u8a2d\u5b9a KNX \u4ecb\u9762" + } + }, + "routing": { + "data": { + "individual_address": "\u500b\u5225\u4f4d\u5740", + "local_ip": "Home Assistant \u672c\u5730\u7aef IP", + "multicast_group": "Multicast \u7fa4\u7d44", + "multicast_port": "Multicast \u901a\u8a0a\u57e0" + }, + "data_description": { + "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", + "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002" + }, + "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002" + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "`.knxkeys` \u6a94\u6848\u5168\u540d\uff08\u5305\u542b\u526f\u6a94\u540d\uff09", + "knxkeys_password": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc" + }, + "data_description": { + "knxkeys_filename": "\u6a94\u6848\u61c9\u8a72\u4f4d\u65bc\u8a2d\u5b9a\u8cc7\u6599\u593e `.storage/knx/` \u5167\u3002\n\u82e5\u70ba Home Assistant OS\u3001\u5247\u61c9\u8a72\u70ba `/config/.storage/knx/`\n\u4f8b\u5982\uff1a`my_project.knxkeys`", + "knxkeys_password": "\u81ea ETS \u532f\u51fa\u6a94\u6848\u4e2d\u9032\u884c\u8a2d\u5b9a\u3002" + }, + "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002" + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "\u88dd\u7f6e\u8a8d\u8b49\u5bc6\u78bc", + "user_id": "\u4f7f\u7528\u8005 ID", + "user_password": "\u4f7f\u7528\u8005\u5bc6\u78bc" + }, + "data_description": { + "device_authentication": "\u65bc EST \u4ecb\u9762\u4e2d 'IP' \u9762\u677f\u9032\u884c\u8a2d\u5b9a\u3002", + "user_id": "\u901a\u5e38\u70ba\u901a\u9053\u6578 +1\u3002\u56e0\u6b64 'Tunnel 2' \u5c07\u5177\u6709\u4f7f\u7528\u8005 ID '3'\u3002", + "user_password": "\u65bc ETS \u901a\u9053 'Properties' \u9762\u677f\u53ef\u8a2d\u5b9a\u6307\u5b9a\u901a\u9053\u9023\u7dda\u5bc6\u78bc\u3002" + }, + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + }, + "secure_tunneling": { + "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", + "menu_options": { + "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", + "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000" + } + }, "tunnel": { "data": { + "gateway": "KNX \u901a\u9053\u9023\u7dda", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" @@ -114,7 +230,8 @@ "data_description": { "host": "KNX/IP \u901a\u9053\u88dd\u7f6e IP \u4f4d\u5740\u3002", "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002" - } + }, + "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" } } } diff --git a/homeassistant/components/nibe_heatpump/translations/ca.json b/homeassistant/components/nibe_heatpump/translations/ca.json index 7dcf70cc17d27b..de5814b4f38998 100644 --- a/homeassistant/components/nibe_heatpump/translations/ca.json +++ b/homeassistant/components/nibe_heatpump/translations/ca.json @@ -18,6 +18,22 @@ "model": "Model de bomba de calor" } }, + "nibegw": { + "data": { + "ip_address": "Adre\u00e7a remota", + "listening_port": "Port local d'escolta", + "model": "Model de bomba de calor", + "remote_read_port": "Port remot de lectura", + "remote_write_port": "Port remot d'escriptura" + }, + "data_description": { + "ip_address": "Adre\u00e7a de la unitat NibeGW. El dispositiu hauria d'estar configurat amb una adre\u00e7a est\u00e0tica.", + "listening_port": "Port local d'aquest sistema al qual la unitat NibeGW est\u00e0 configurada per enviar-hi dades.", + "remote_read_port": "Port on la unitat NibeGW espera les sol\u00b7licituds de lectura.", + "remote_write_port": "Port on la unitat NibeGW espera les sol\u00b7licituds d'escriptura." + }, + "description": "Abans d'intentar configurar la integraci\u00f3, comprova que:\n - La unitat NibeGW est\u00e0 connectada a una bomba de calor.\n - S'ha activat l'accessori MODBUS40 a la configuraci\u00f3 de la bomba de calor.\n - La bomba no ha entrat en estat d'alarma per falta de l'accessori MODBUS40." + }, "user": { "data": { "ip_address": "Adre\u00e7a remota", diff --git a/homeassistant/components/nibe_heatpump/translations/de.json b/homeassistant/components/nibe_heatpump/translations/de.json index e5f98b60bddf1d..4e1fc0ce17bd6f 100644 --- a/homeassistant/components/nibe_heatpump/translations/de.json +++ b/homeassistant/components/nibe_heatpump/translations/de.json @@ -6,11 +6,11 @@ "error": { "address": "Ung\u00fcltige Remote-Adresse angegeben. Die Adresse muss eine IP-Adresse oder ein aufl\u00f6sbarer Hostname sein.", "address_in_use": "Der ausgew\u00e4hlte Listening-Port wird auf diesem System bereits verwendet.", - "model": "Das ausgew\u00e4hlte Modell scheint modbus40 nicht zu unterst\u00fctzen", - "read": "Fehler bei Leseanforderung von Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Leseport\u201c oder \u201eRemote-IP-Adresse\u201c.", + "model": "Das ausgew\u00e4hlte Modell scheint MODBUS40 nicht zu unterst\u00fctzen", + "read": "Fehler bei Leseanforderung von Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Leseport\u201c oder \u201eRemote-Adresse\u201c.", "unknown": "Unerwarteter Fehler", - "url": "Die angegebene URL ist keine wohlgeformte und unterst\u00fctzte URL", - "write": "Fehler bei Schreibanforderung an Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Schreibport\u201c oder \u201eRemote-IP-Adresse\u201c." + "url": "Die angegebene URL ist weder wohlgeformt noch wird sie unterst\u00fctzt.", + "write": "Fehler bei Schreibanforderung an Pumpe. \u00dcberpr\u00fcfe deinen \u201eRemote-Schreibport\u201c oder \u201eRemote-Adresse\u201c." }, "step": { "modbus": { @@ -53,7 +53,7 @@ "remote_read_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Leseanfragen wartet.", "remote_write_port": "Der Port, an dem das NibeGW-Ger\u00e4t auf Schreibanfragen wartet." }, - "description": "W\u00e4hle die Verbindungsmethode zu deiner Pumpe. Im Allgemeinen erfordern Pumpen der F-Serie ein kundenspezifisches Nibe GW-Zubeh\u00f6r, w\u00e4hrend eine Pumpe der S-Serie \u00fcber eine integrierte Modbus-Unterst\u00fctzung verf\u00fcgt.", + "description": "W\u00e4hle die Verbindungsmethode zu deiner Pumpe. Im Allgemeinen erfordern Pumpen der F-Serie ein kundenspezifisches NibeGW-Zubeh\u00f6r, w\u00e4hrend eine Pumpe der S-Serie \u00fcber eine integrierte Modbus-Unterst\u00fctzung verf\u00fcgt.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/nibe_heatpump/translations/en.json b/homeassistant/components/nibe_heatpump/translations/en.json index afe50efb61ab7a..3b50bb0986a98c 100644 --- a/homeassistant/components/nibe_heatpump/translations/en.json +++ b/homeassistant/components/nibe_heatpump/translations/en.json @@ -61,4 +61,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/es.json b/homeassistant/components/nibe_heatpump/translations/es.json index 0365e60adea16b..c241724f14a22a 100644 --- a/homeassistant/components/nibe_heatpump/translations/es.json +++ b/homeassistant/components/nibe_heatpump/translations/es.json @@ -6,11 +6,11 @@ "error": { "address": "Se especific\u00f3 una direcci\u00f3n remota no v\u00e1lida. La direcci\u00f3n debe ser una direcci\u00f3n IP o un nombre de host resoluble.", "address_in_use": "El puerto de escucha seleccionado ya est\u00e1 en uso en este sistema.", - "model": "El modelo seleccionado no parece ser compatible con modbus40", - "read": "Error en la solicitud de lectura de la bomba. Verifica tu `Puerto de lectura remoto` o `Direcci\u00f3n IP remota`.", + "model": "El modelo seleccionado no parece ser compatible con MODBUS40", + "read": "Error en la solicitud de lectura de la bomba. Verifica tu `Puerto de lectura remoto` o `Direcci\u00f3n remota`.", "unknown": "Error inesperado", - "url": "La URL especificada no es una URL bien formada y compatible", - "write": "Error en la solicitud de escritura a la bomba. Verifica tu `Puerto de escritura remoto` o `Direcci\u00f3n IP remota`." + "url": "La URL especificada no est\u00e1 bien formada ni es compatible", + "write": "Error en la solicitud de escritura a la bomba. Verifica tu `Puerto de escritura remoto` o `Direcci\u00f3n remota`." }, "step": { "modbus": { @@ -20,7 +20,7 @@ "model": "Modelo de bomba de calor" }, "data_description": { - "modbus_unit": "Identificaci\u00f3n de la unidad para su bomba de calor. Por lo general, se puede dejar en 0.", + "modbus_unit": "Identificaci\u00f3n de la unidad para tu bomba de calor. Por lo general, se puede dejar en 0.", "modbus_url": "URL Modbus que describe la conexi\u00f3n a tu bomba de calor o unidad MODBUS40. Debe estar en el formato:\n - `tcp://[HOST]:[PUERTO]` para conexi\u00f3n Modbus TCP\n - `serial://[DISPOSITIVO LOCAL]` para una conexi\u00f3n Modbus RTU local\n - `rfc2217://[HOST]:[PUERTO]` para una conexi\u00f3n remota Modbus RTU basada en telnet." } }, @@ -53,7 +53,7 @@ "remote_read_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando las peticiones de lectura.", "remote_write_port": "El puerto en el que la unidad NibeGW est\u00e1 escuchando peticiones de escritura." }, - "description": "Elige el m\u00e9todo de conexi\u00f3n a tu bomba. En general, las bombas de la serie F requieren un accesorio personalizado Nibe GW, mientras que una bomba de la serie S tiene soporte Modbus incorporado.", + "description": "Elige el m\u00e9todo de conexi\u00f3n a tu bomba. En general, las bombas de la serie F requieren un accesorio personalizado NibeGW, mientras que una bomba de la serie S tiene soporte Modbus incorporado.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/nibe_heatpump/translations/et.json b/homeassistant/components/nibe_heatpump/translations/et.json index 8fff7663bb02d1..56ffec08aece8e 100644 --- a/homeassistant/components/nibe_heatpump/translations/et.json +++ b/homeassistant/components/nibe_heatpump/translations/et.json @@ -6,10 +6,10 @@ "error": { "address": "M\u00e4\u00e4ratud vale kaugaadress. Aadress peab olema IP-aadress v\u00f5i lahendatav hostinimi.", "address_in_use": "Valitud kuulamisport on selles s\u00fcsteemis juba kasutusel.", - "model": "Valitud mudel ei n\u00e4i toetavat modbus40.", - "read": "Viga pumba lugemistaotlusel. Kinnitage oma \"Kaugloetav port\" v\u00f5i \"Kaug-IP-aadress\".", + "model": "Valitud mudel ei n\u00e4i toetavat MODBUS40", + "read": "Viga pumba lugemistaotlusel. Kinnita oma \"Kaugloetav port\" v\u00f5i \"Kaug-IP-aadress\".", "unknown": "Ootamatu t\u00f5rge", - "url": "M\u00e4\u00e4ratud URL ei ole h\u00e4sti vormindatud ja toetatud URL", + "url": "M\u00e4\u00e4ratud URL ei ole h\u00e4sti vormindatud ja toetatud", "write": "Viga pumba kirjutamise taotlusel. Kontrollige oma `kaugkirjutusport` v\u00f5i `kaug-IP-aadress`." }, "step": { diff --git a/homeassistant/components/nibe_heatpump/translations/id.json b/homeassistant/components/nibe_heatpump/translations/id.json index 44e0a8c024732d..7a5657508fc2a1 100644 --- a/homeassistant/components/nibe_heatpump/translations/id.json +++ b/homeassistant/components/nibe_heatpump/translations/id.json @@ -6,10 +6,11 @@ "error": { "address": "Alamat IP jarak jauh yang ditentukan tidak valid. Alamat harus berupa alamat IP atau nama host yang dapat ditemukan.", "address_in_use": "Port mendengarkan yang dipilih sudah digunakan pada sistem ini.", - "model": "Model yang dipilih tampaknya tidak mendukung modbus40", - "read": "Kesalahan pada permintaan baca dari pompa. Verifikasi `Port baca jarak jauh` atau `Alamat IP jarak jauh` Anda.", + "model": "Model yang dipilih tampaknya tidak mendukung MODBUS40", + "read": "Kesalahan pada permintaan baca dari pompa. Verifikasi `Port baca jarak jauh` atau `Alamat jarak jauh` Anda.", "unknown": "Kesalahan yang tidak diharapkan", - "write": "Kesalahan pada permintaan tulis ke pompa. Verifikasi `Port tulis jarak jauh` atau `Alamat IP jarak jauh` Anda." + "url": "URL yang ditentukan tidak dalam format yang benar atau tidak didukung", + "write": "Kesalahan pada permintaan tulis ke pompa. Verifikasi `Port tulis jarak jauh` atau `Alamat jarak jauh` Anda." }, "step": { "modbus": { @@ -17,6 +18,10 @@ "modbus_unit": "Pengidentifikasi Unit Modbus", "modbus_url": "URL Modbus", "model": "Model Pompa Panas" + }, + "data_description": { + "modbus_unit": "Identifikasi unit untuk Pompa Panas Anda. Biasanya dapat dibiarkan pada nilai 0.", + "modbus_url": "URL Modbus yang menjelaskan koneksi ke unit Heat Pump atau MODBUS40 Anda. Ini harus dalam format:\n - `tcp://[HOST]:[PORT]` untuk koneksi Modbus TCP\n - `serial://[LOCAL DEVICE]` untuk koneksi Modbus RTU lokal\n - `rfc2217://[HOST]:[PORT]` untuk koneksi Modbus RTU berbasis telnet jarak jauh." } }, "nibegw": { @@ -29,7 +34,9 @@ }, "data_description": { "ip_address": "Alamat unit NibeGW. Perangkat seharusnya sudah dikonfigurasi dengan alamat statis.", - "listening_port": "Port lokal pada sistem ini, yang dikonfigurasi untuk mengirim data ke unit NibeGW." + "listening_port": "Port lokal pada sistem ini, yang dikonfigurasi untuk mengirim data ke unit NibeGW.", + "remote_read_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan baca.", + "remote_write_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan tulis." }, "description": "Sebelum mencoba mengonfigurasi integrasi, pastikan bahwa:\n - Unit NibeGW terhubung ke pompa panas.\n - Aksesori MODBUS40 telah diaktifkan dalam konfigurasi pompa panas.\n - Pompa tidak sedang dalam status alarm tentang aksesori MODBUS40 yang tidak tersedia." }, @@ -46,7 +53,7 @@ "remote_read_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan baca.", "remote_write_port": "Port yang digunakan unit NibeGW untuk mendengarkan permintaan tulis." }, - "description": "Pilih metode koneksi ke pompa. Secara umum, pompa seri F memerlukan aksesori khusus Nibe GW, sementara pompa seri S memiliki dukungan Modbus bawaan.", + "description": "Pilih metode koneksi ke pompa. Secara umum, pompa seri F memerlukan aksesori khusus NibeGW, sementara pompa seri S memiliki dukungan Modbus bawaan.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/nibe_heatpump/translations/it.json b/homeassistant/components/nibe_heatpump/translations/it.json index 81da8c2503d6b8..96b686f9818968 100644 --- a/homeassistant/components/nibe_heatpump/translations/it.json +++ b/homeassistant/components/nibe_heatpump/translations/it.json @@ -6,11 +6,11 @@ "error": { "address": "Indirizzo remoto specificato non valido. L'indirizzo deve essere un indirizzo IP o un nome host risolvibile.", "address_in_use": "La porta di ascolto selezionata \u00e8 gi\u00e0 in uso su questo sistema.", - "model": "Il modello selezionato non sembra supportare il modbus40", - "read": "Errore su richiesta di lettura dalla pompa. Verifica la tua \"Porta di lettura remota\" o \"Indirizzo IP remoto\".", + "model": "Il modello selezionato non sembra supportare il MODBUS40.", + "read": "Errore nella richiesta di lettura da parte della pompa. Verifica la tua \"Porta di lettura remota\" o \"Indirizzo remoto\".", "unknown": "Errore imprevisto", - "url": "L'URL specificato non \u00e8 un URL ben formato e supportato", - "write": "Errore nella richiesta di scrittura alla pompa. Verifica la tua \"Porta di scrittura remota\" o \"Indirizzo IP remoto\"." + "url": "L'URL specificato non \u00e8 correttamente formato n\u00e9 supportato", + "write": "Errore nella richiesta di scrittura alla pompa. Verifica la tua \"Porta di scrittura remota\" o \"Indirizzo remoto\"." }, "step": { "modbus": { @@ -20,7 +20,7 @@ "model": "Modello di pompa di calore" }, "data_description": { - "modbus_unit": "Identificazione dell'unit\u00e0 per la tua pompa di calore. Di solito pu\u00f2 essere lasciato a 0.", + "modbus_unit": "Identificazione dell'unit\u00e0 per la pompa di calore. Di solito pu\u00f2 essere lasciato a 0.", "modbus_url": "Modbus URL che descrive la connessione alla pompa di calore o all'unit\u00e0 MODBUS40. Dovrebbe essere nella forma:\n - `tcp://[HOST]:[PORTA]` per la connessione Modbus TCP\n - `serial://[DISPOSITIVO LOCALE]` per una connessione Modbus RTU locale\n - `rfc2217://[HOST]:[PORTA]` per una connessione Modbus RTU remota basata su telnet." } }, @@ -53,7 +53,7 @@ "remote_read_port": "La porta su cui l'unit\u00e0 NibeGW \u00e8 in ascolto per le richieste di lettura.", "remote_write_port": "La porta su cui l'unit\u00e0 NibeGW \u00e8 in ascolto per le richieste di scrittura." }, - "description": "Scegli il metodo di connessione alla tua pompa. In generale, le pompe della serie F richiedono un accessorio personalizzato Nibe GW, mentre le pompe della serie S hanno il supporto Modbus integrato.", + "description": "Scegli il metodo di connessione alla tua pompa. In generale, le pompe della serie F richiedono un accessorio personalizzato NibeGW, mentre una pompa della serie S ha il supporto Modbus integrato.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/nibe_heatpump/translations/pl.json b/homeassistant/components/nibe_heatpump/translations/pl.json index a6cc0785e97032..e97517d2a226e7 100644 --- a/homeassistant/components/nibe_heatpump/translations/pl.json +++ b/homeassistant/components/nibe_heatpump/translations/pl.json @@ -6,10 +6,10 @@ "error": { "address": "Podano nieprawid\u0142owy zdalny adres IP. Adres musi by\u0107 adresem IP lub rozpoznawaln\u0105 nazw\u0105 hosta.", "address_in_use": "Wybrany port nas\u0142uchiwania jest ju\u017c u\u017cywany w tym systemie.", - "model": "Wybrany model nie obs\u0142uguje modbus40", + "model": "Wybrany model nie obs\u0142uguje MODBUS40", "read": "B\u0142\u0105d przy \u017c\u0105daniu odczytu z pompy. Sprawd\u017a \u201eZdalny port odczytu\u201d lub \u201eZdalny adres IP\u201d.", "unknown": "Nieoczekiwany b\u0142\u0105d", - "url": "Podany adres URL nie jest poprawnie sformu\u0142owanym i obs\u0142ugiwanym adresem URL", + "url": "Podany adres URL nie jest poprawnie sformu\u0142owany lub obs\u0142ugiwany", "write": "B\u0142\u0105d przy \u017c\u0105daniu zapisu do pompy. Sprawd\u017a \u201eZdalny port zapisu\u201d lub \u201eZdalny adres IP\u201d." }, "step": { @@ -53,9 +53,9 @@ "remote_read_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 odczytu.", "remote_write_port": "Port, na kt\u00f3rym urz\u0105dzenie NibeGW nas\u0142uchuje \u017c\u0105da\u0144 zapisu." }, - "description": "Wybierz metod\u0119 po\u0142\u0105czenia z pomp\u0105. Og\u00f3lnie rzecz bior\u0105c, pompy serii F wymagaj\u0105 niestandardowego akcesorium Nibe GW, podczas gdy pompy serii S maj\u0105 wbudowan\u0105 obs\u0142ug\u0119 protoko\u0142u Modbus.", + "description": "Wybierz metod\u0119 po\u0142\u0105czenia z pomp\u0105. Og\u00f3lnie rzecz bior\u0105c, pompy serii F wymagaj\u0105 niestandardowego akcesorium NibeGW, podczas gdy pompy serii S maj\u0105 wbudowan\u0105 obs\u0142ug\u0119 protoko\u0142u Modbus.", "menu_options": { - "modbus": "Modbus", + "modbus": "MODBUS", "nibegw": "NibeGW" } } diff --git a/homeassistant/components/nibe_heatpump/translations/pt-BR.json b/homeassistant/components/nibe_heatpump/translations/pt-BR.json index 1ae021ee11b4b8..6059385b861740 100644 --- a/homeassistant/components/nibe_heatpump/translations/pt-BR.json +++ b/homeassistant/components/nibe_heatpump/translations/pt-BR.json @@ -6,11 +6,11 @@ "error": { "address": "Endere\u00e7o remoto inv\u00e1lido especificado. O endere\u00e7o deve ser um endere\u00e7o IP ou um nome de host resolv\u00edvel.", "address_in_use": "A porta de escuta selecionada j\u00e1 est\u00e1 em uso neste sistema.", - "model": "O modelo selecionado parece n\u00e3o suportar modbus40", - "read": "Erro na solicita\u00e7\u00e3o de leitura da bomba. Verifique sua `Porta de leitura remota` ou `Endere\u00e7o IP remoto`.", + "model": "O modelo selecionado parece n\u00e3o suportar MODBUS40", + "read": "Erro na solicita\u00e7\u00e3o de leitura da bomba. Verifique sua `Porta de leitura remota` ou `Endere\u00e7o remoto`.", "unknown": "Erro inesperado", "url": "A URL especificada n\u00e3o \u00e9 uma URL bem formada e suportada", - "write": "Erro na solicita\u00e7\u00e3o de grava\u00e7\u00e3o para bombear. Verifique sua `Porta de grava\u00e7\u00e3o remota` ou `Endere\u00e7o IP remoto`." + "write": "Erro na solicita\u00e7\u00e3o de grava\u00e7\u00e3o para bombear. Verifique sua `Porta de grava\u00e7\u00e3o remota` ou `Endere\u00e7o remoto`." }, "step": { "modbus": { @@ -53,7 +53,7 @@ "remote_read_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de leitura.", "remote_write_port": "A porta na qual a unidade NibeGW est\u00e1 escutando solicita\u00e7\u00f5es de grava\u00e7\u00e3o." }, - "description": "Escolha o m\u00e9todo de conex\u00e3o para sua bomba. Em geral, as bombas da s\u00e9rie F requerem um acess\u00f3rio personalizado Nibe GW, enquanto uma bomba da s\u00e9rie S possui suporte Modbus integrado.", + "description": "Escolha o m\u00e9todo de conex\u00e3o para sua bomba. Em geral, as bombas da s\u00e9rie F requerem um acess\u00f3rio personalizado NibeGW, enquanto uma bomba da s\u00e9rie S tem suporte Modbus integrado.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/nibe_heatpump/translations/ru.json b/homeassistant/components/nibe_heatpump/translations/ru.json index 42c7a853c94a63..de67991f5af7e3 100644 --- a/homeassistant/components/nibe_heatpump/translations/ru.json +++ b/homeassistant/components/nibe_heatpump/translations/ru.json @@ -6,11 +6,11 @@ "error": { "address": "\u0423\u043a\u0430\u0437\u0430\u043d \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441. \u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430.", "address_in_use": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u043d\u0438\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u044d\u0442\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.", - "model": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 modbus40.", - "read": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`.", + "model": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 MODBUS40.", + "read": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0447\u0442\u0435\u043d\u0438\u044f` \u0438\u043b\u0438 `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441`.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "url": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0441\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", - "write": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438` \u0438\u043b\u0438 `\u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441`." + "write": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u0437\u0430\u043f\u0438\u0441\u0438` \u0438\u043b\u0438 `\u0423\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441`." }, "step": { "modbus": { @@ -53,7 +53,7 @@ "remote_read_port": "\u041f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0447\u0442\u0435\u043d\u0438\u0435.", "remote_write_port": "\u041f\u043e\u0440\u0442, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e NibeGW \u043f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c." }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443. \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0434\u043b\u044f \u043d\u0430\u0441\u043e\u0441\u043e\u0432 \u0441\u0435\u0440\u0438\u0438 F \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 Nibe GW, \u0432 \u0442\u043e \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043d\u0430\u0441\u043e\u0441\u044b \u0441\u0435\u0440\u0438\u0438 S \u0438\u043c\u0435\u044e\u0442 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 Modbus.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u043d\u0430\u0441\u043e\u0441\u0443. \u041a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0434\u043b\u044f \u043d\u0430\u0441\u043e\u0441\u043e\u0432 \u0441\u0435\u0440\u0438\u0438 F \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 NibeGW, \u0432 \u0442\u043e \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043d\u0430\u0441\u043e\u0441\u044b \u0441\u0435\u0440\u0438\u0438 S \u0438\u043c\u0435\u044e\u0442 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 Modbus.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json index fe182397efa4da..ff9cb6fe24f9e1 100644 --- a/homeassistant/components/nibe_heatpump/translations/zh-Hant.json +++ b/homeassistant/components/nibe_heatpump/translations/zh-Hant.json @@ -6,11 +6,11 @@ "error": { "address": "\u6307\u5b9a\u7684\u9060\u7aef\u4f4d\u5740\u7121\u6548\u3002\u4f4d\u5740\u5fc5\u9808\u70ba IP \u4f4d\u5740\u6216\u53ef\u89e3\u6790\u7684\u4e3b\u6a5f\u540d\u7a31\u3002", "address_in_use": "\u6240\u9078\u64c7\u7684\u76e3\u807d\u901a\u8a0a\u57e0\u5df2\u7d93\u88ab\u7cfb\u7d71\u6240\u4f7f\u7528\u3002", - "model": "\u6240\u9078\u64c7\u7684\u578b\u865f\u4f3c\u4e4e\u4e0d\u652f\u63f4 modbus40", - "read": "\u8b80\u53d6\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u8b80\u53d6\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002", + "model": "\u6240\u9078\u64c7\u7684\u578b\u865f\u4f3c\u4e4e\u4e0d\u652f\u63f4 MODBUS40", + "read": "\u8b80\u53d6\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u8b80\u53d6\u57e0` \u6216 `\u9060\u7aef\u4f4d\u5740`\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "url": "\u6307\u5b9a\u7684 URL \u4e0d\u662f\u6b63\u78ba\u683c\u5f0f\u6216\u652f\u63f4\u7684URL", - "write": "\u5beb\u5165\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u5beb\u5165\u57e0` \u6216 `\u9060\u7aef IP \u4f4d\u5740`\u3002" + "url": "\u6307\u5b9a\u7684 URL \u4e0d\u662f\u6b63\u78ba\u683c\u5f0f\u6216\u652f\u63f4\u7684 URL", + "write": "\u5beb\u5165\u8acb\u6c42\u767c\u751f\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d `\u9060\u7aef\u5beb\u5165\u57e0` \u6216 `\u9060\u7aef\u4f4d\u5740`\u3002" }, "step": { "modbus": { diff --git a/homeassistant/components/openuv/translations/bg.json b/homeassistant/components/openuv/translations/bg.json index 06c8d9cde09377..64e6e35e73767c 100644 --- a/homeassistant/components/openuv/translations/bg.json +++ b/homeassistant/components/openuv/translations/bg.json @@ -12,6 +12,7 @@ "data": { "api_key": "API \u043a\u043b\u044e\u0447" }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e API \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 {latitude}, {longitude}.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, "user": { From 777bf27dda6a0a4d52db8205c5899532c3bf1217 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 01:51:58 -0600 Subject: [PATCH 376/394] Bump bleak-retry-connector to 2.8.4 (#81937) changelog: https://github.com/Bluetooth-Devices/bleak-retry-connector/compare/v2.8.3...v2.8.4 reduces the risk that the operation will fail because the adapter temporarily runs out of connection slots --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index cc7237d458596d..59397f30bf9f69 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.8.3", + "bleak-retry-connector==2.8.4", "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "bluetooth-data-tools==0.2.0", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 66ffb1eb85ba75..daeeda866a85f8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.3 +bleak-retry-connector==2.8.4 bleak==0.19.2 bluetooth-adapters==0.7.0 bluetooth-auto-recovery==0.3.6 diff --git a/requirements_all.txt b/requirements_all.txt index 17351f986a27c0..e8b5e7083c9202 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.3 +bleak-retry-connector==2.8.4 # homeassistant.components.bluetooth bleak==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a4276019e99e4..6126fe55723c09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ bellows==0.34.2 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.3 +bleak-retry-connector==2.8.4 # homeassistant.components.bluetooth bleak==0.19.2 From 8c9a8d72a8cdb178020b246c1d3511c9baeebd94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 01:53:07 -0600 Subject: [PATCH 377/394] Bump PySwitchbot to 0.20.3 (#81938) changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.20.2...0.20.3 releases the connection sooner to reduce the risk of running out of connection slots on the ble adapter --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 532edac7d43163..2c95327beefd63 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.20.2"], + "requirements": ["PySwitchbot==0.20.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e8b5e7083c9202..232acb2c6d3bf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.2 +PySwitchbot==0.20.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6126fe55723c09..9987c7a1e9b110 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.20.2 +PySwitchbot==0.20.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 6517e3e21e5828a317ea8b814d0345f2536b9170 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 02:09:28 -0600 Subject: [PATCH 378/394] Fix esphome bleak client seeing disconnects too late (#81932) * Fix esphome bleak client seeing disconnects too late Because allbacks are delivered asynchronously its possible that we find out during the operation before the callback is delivered telling us about the disconnected. We now watch for error code -1 which indicates the connection dropped out from under us * debug logging * cleanup comment * Fix comment grammar Co-authored-by: Martin Hjelmare --- .../components/esphome/bluetooth/client.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index c6b60831577ce8..ceac4e5aaae73c 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -13,6 +13,7 @@ BLEConnectionError, ) from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError +from aioesphomeapi.core import BluetoothGATTAPIError import async_timeout from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.client import BaseBleakClient, NotifyCallback @@ -83,6 +84,24 @@ async def _async_wrap_bluetooth_operation( return await func(self, *args, **kwargs) except TimeoutAPIError as err: raise asyncio.TimeoutError(str(err)) from err + except BluetoothGATTAPIError as ex: + # If the device disconnects in the middle of an operation + # be sure to mark it as disconnected so any library using + # the proxy knows to reconnect. + # + # Because callbacks are delivered asynchronously it's possible + # that we find out about the disconnection during the operation + # before the callback is delivered. + if ex.error.error == -1: + _LOGGER.debug( + "%s: %s - %s: BLE device disconnected during %s operation", + self._source, # pylint: disable=protected-access + self._ble_device.name, # pylint: disable=protected-access + self._ble_device.address, # pylint: disable=protected-access + func.__name__, + ) + self._async_ble_device_disconnected() # pylint: disable=protected-access + raise BleakError(str(ex)) from ex except APIConnectionError as err: raise BleakError(str(err)) from err From 89959e7cda75400144ef1d3ec7c11ba651b8d085 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 07:59:06 -0600 Subject: [PATCH 379/394] Reduce complexity of bluetooth scanners for local adapters (#81940) This is a small refactor to align the bluetooth scanner class to the same design used in the esphome scanner to pass the callback for new service infos in the constructor. The original thinking was that multiple callbacks would be needed to send the service infos to different paths, however with the creation of a central manager, this designed turned out to not be needed and can be simplified --- .../components/bluetooth/__init__.py | 5 +- homeassistant/components/bluetooth/scanner.py | 47 +++++++------------ 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8590d1ad90a341..23165e2bc0966c 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -425,15 +425,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: passive = entry.options.get(CONF_PASSIVE) mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE - scanner = HaScanner(hass, mode, adapter, address) + new_info_callback = async_get_advertisement_callback(hass) + scanner = HaScanner(hass, mode, adapter, address, new_info_callback) try: scanner.async_setup() except RuntimeError as err: raise ConfigEntryNotReady( f"{adapter_human_name(adapter, address)}: {err}" ) from err - info_callback = async_get_advertisement_callback(hass) - entry.async_on_unload(scanner.async_register_callback(info_callback)) try: await scanner.async_start() except ScannerStartError as err: diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 43c4706474aa33..c05894d3b4489d 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -125,6 +125,7 @@ def __init__( mode: BluetoothScanningMode, adapter: str, address: str, + new_info_callback: Callable[[BluetoothServiceInfoBleak], None], ) -> None: """Init bluetooth discovery.""" source = address if address != DEFAULT_ADDRESS else adapter or SOURCE_LOCAL @@ -135,7 +136,7 @@ def __init__( self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 self._start_time = 0.0 - self._callbacks: list[Callable[[BluetoothServiceInfoBleak], None]] = [] + self._new_info_callback = new_info_callback self.name = adapter_human_name(adapter, address) @property @@ -168,22 +169,6 @@ async def async_diagnostics(self) -> dict[str, Any]: "start_time": self._start_time, } - @hass_callback - def async_register_callback( - self, callback: Callable[[BluetoothServiceInfoBleak], None] - ) -> CALLBACK_TYPE: - """Register a callback. - - Currently this is used to feed the callbacks into the - central manager. - """ - - def _remove() -> None: - self._callbacks.remove(callback) - - self._callbacks.append(callback) - return _remove - @hass_callback def _async_detection_callback( self, @@ -206,21 +191,21 @@ def _async_detection_callback( # as the adapter is in a failure # state if all the data is empty. self._last_detection = callback_time - service_info = BluetoothServiceInfoBleak( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=advertisement_data.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=self.source, - device=device, - advertisement=advertisement_data, - connectable=True, - time=callback_time, + self._new_info_callback( + BluetoothServiceInfoBleak( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=advertisement_data.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=self.source, + device=device, + advertisement=advertisement_data, + connectable=True, + time=callback_time, + ) ) - for callback in self._callbacks: - callback(service_info) async def async_start(self) -> None: """Start bluetooth scanner.""" From b5dfe8c6b978b0277151c17b46661811b0a06868 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 11 Nov 2022 15:37:57 +0000 Subject: [PATCH 380/394] Fix battery %, battery voltage and signal strength not being diagnostic entities in xiaomi_ble (#81960) --- homeassistant/components/xiaomi_ble/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 1aa1b7f8f722c6..0e7e05a3a94b8d 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -29,6 +29,7 @@ TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -64,12 +65,14 @@ device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( key=str(Units.ELECTRIC_POTENTIAL_VOLT), device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, ), ( DeviceClass.SIGNAL_STRENGTH, @@ -80,6 +83,7 @@ native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, ), # Used for e.g. moisture sensor on HHCCJCY01 (None, Units.PERCENTAGE): SensorEntityDescription( From 1baa5c12c0c4802edf01a64964b0ed94b7e2c4a7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 11 Nov 2022 17:08:26 +0100 Subject: [PATCH 381/394] Fix rest schema (#81857) --- homeassistant/components/rest/schema.py | 3 +-- tests/components/rest/test_init.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d124ce5789c9e8..cfd8f8a3852762 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -91,9 +91,8 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( - # convert empty dict to empty list - lambda x: [] if x == {} else x, cv.ensure_list, + cv.remove_falsy, [COMBINED_SCHEMA], ) }, diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 08d538fd163e6c..fbee3c2051a136 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -418,3 +418,19 @@ async def test_empty_config(hass: HomeAssistant) -> None: {DOMAIN: {}}, ) assert_setup_component(0, DOMAIN) + + +async def test_config_schema_via_packages(hass: HomeAssistant) -> None: + """Test configuration via packages.""" + packages = { + "pack_dict": {"rest": {}}, + "pack_11": {"rest": {"resource": "http://url1"}}, + "pack_list": {"rest": [{"resource": "http://url2"}]}, + } + config = {hass_config.CONF_CORE: {hass_config.CONF_PACKAGES: packages}} + await hass_config.merge_packages_config(hass, config, packages) + + assert len(config) == 2 + assert len(config["rest"]) == 2 + assert config["rest"][0]["resource"] == "http://url1" + assert config["rest"][1]["resource"] == "http://url2" From 380ae12997f91d2f63bb50f663677a0c51a35784 Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Fri, 11 Nov 2022 22:01:15 +0000 Subject: [PATCH 382/394] Fix Growatt missing state class (#81980) Growatt - Fix #80003 - Add missing state class --- homeassistant/components/growatt_server/sensor_types/tlx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/growatt_server/sensor_types/tlx.py b/homeassistant/components/growatt_server/sensor_types/tlx.py index 597ddd789cf94c..ba455747457e83 100644 --- a/homeassistant/components/growatt_server/sensor_types/tlx.py +++ b/homeassistant/components/growatt_server/sensor_types/tlx.py @@ -20,6 +20,7 @@ api_key="eacToday", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, precision=1, ), GrowattSensorEntityDescription( From a8aea6df5ea222f7cb960fb847f349a0cc771eb1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 16:01:54 -0600 Subject: [PATCH 383/394] Bump oralb-ble to 0.14.2 (#81969) fixes detection of the black 9000 model fixes #81967 changelog: https://github.com/Bluetooth-Devices/oralb-ble/compare/v0.14.1...v0.14.2 --- homeassistant/components/oralb/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index eff6c999c30fa3..8868330a7e7bb1 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,7 +8,7 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.14.1"], + "requirements": ["oralb-ble==0.14.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 232acb2c6d3bf9..c80978cd46473c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1255,7 +1255,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.14.1 +oralb-ble==0.14.2 # homeassistant.components.oru oru==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9987c7a1e9b110..299050fbc7b959 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -900,7 +900,7 @@ open-meteo==0.2.1 openerz-api==0.1.0 # homeassistant.components.oralb -oralb-ble==0.14.1 +oralb-ble==0.14.2 # homeassistant.components.ovo_energy ovoenergy==1.2.0 From 8ee60c2e7f47e09d980fa2a5144482c8402a5857 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 11 Nov 2022 17:03:32 -0500 Subject: [PATCH 384/394] Bump ZHA quirks lib to 0.0.86 (#81966) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c8aebe3b0c0a66..312b93aff6f23f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.34.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.85", + "zha-quirks==0.0.86", "zigpy-deconz==0.19.0", "zigpy==0.51.5", "zigpy-xbee==0.16.2", diff --git a/requirements_all.txt b/requirements_all.txt index c80978cd46473c..3a1b515876e348 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2624,7 +2624,7 @@ zengge==0.2 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.85 +zha-quirks==0.0.86 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 299050fbc7b959..158501504e10c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1825,7 +1825,7 @@ zamg==0.1.1 zeroconf==0.39.4 # homeassistant.components.zha -zha-quirks==0.0.85 +zha-quirks==0.0.86 # homeassistant.components.zha zigpy-deconz==0.19.0 From de48ae2e53f5b35df5892df1f96daf033198a5bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Nov 2022 16:22:22 -0600 Subject: [PATCH 385/394] Bump dbus-fast to 1.73.0 (#81959) changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v1.72.0...v1.73.0 --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 59397f30bf9f69..e2daee3650d7d7 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -11,7 +11,7 @@ "bluetooth-adapters==0.7.0", "bluetooth-auto-recovery==0.3.6", "bluetooth-data-tools==0.2.0", - "dbus-fast==1.72.0" + "dbus-fast==1.73.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index daeeda866a85f8..603d1bba36a34c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ bluetooth-data-tools==0.2.0 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==38.0.3 -dbus-fast==1.72.0 +dbus-fast==1.73.0 fnvhash==0.1.0 hass-nabucasa==0.56.0 home-assistant-bluetooth==1.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3a1b515876e348..ead4f05bf9d042 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.72.0 +dbus-fast==1.73.0 # homeassistant.components.debugpy debugpy==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 158501504e10c7..d8661001022ac8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -433,7 +433,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.72.0 +dbus-fast==1.73.0 # homeassistant.components.debugpy debugpy==1.6.3 From a6a3bf261d89c7133841ae0fd8e1e24f2358a7c5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 12 Nov 2022 00:28:24 +0000 Subject: [PATCH 386/394] [ci skip] Translation update --- .../components/braviatv/translations/ca.json | 3 + .../components/generic/translations/bg.json | 2 + .../homeassistant/translations/ca.json | 6 + .../components/knx/translations/bg.json | 14 +- .../components/knx/translations/no.json | 125 +++++++++++++++++- .../components/livisi/translations/ca.json | 4 +- .../components/mjpeg/translations/bg.json | 2 + .../components/mqtt/translations/ca.json | 16 +-- .../nibe_heatpump/translations/ca.json | 14 +- .../nibe_heatpump/translations/no.json | 10 +- .../transmission/translations/ca.json | 13 ++ 11 files changed, 186 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index e6f8ebc8e9204c..2b15a496af0005 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -41,6 +41,9 @@ } }, "options": { + "abort": { + "failed_update": "S'ha produ\u00eft un error en actualitzar la llista de fonts.\n\nAssegura't que el televisor est\u00e0 engegat abans de configurar-lo." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/generic/translations/bg.json b/homeassistant/components/generic/translations/bg.json index b26b65e715e95d..50cb1feb1b7950 100644 --- a/homeassistant/components/generic/translations/bg.json +++ b/homeassistant/components/generic/translations/bg.json @@ -5,6 +5,7 @@ "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { + "no_still_image_or_stream_url": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043f\u043e\u0441\u043e\u0447\u0438\u0442\u0435 \u043f\u043e\u043d\u0435 URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { @@ -22,6 +23,7 @@ "authentication": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "still_image_url": "URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 (\u043d\u0430\u043f\u0440. http://...)", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u043a\u0430\u043c\u0435\u0440\u0430\u0442\u0430." diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index 8bcf5d96a86d50..e850412488a95f 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "El valor de moneda {currency} ja no s'utilitza, modifica la configuraci\u00f3 del valor de moneda.", + "title": "El valor de moneda configurat ja no s'utilitza" + } + }, "system_health": { "info": { "arch": "Arquitectura de la CPU", diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 331c85d40657e1..72bbfb4a80c0dd 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -37,6 +37,10 @@ } }, "options": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_ip_address": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IPv4 \u0430\u0434\u0440\u0435\u0441." + }, "step": { "init": { "data": { @@ -46,12 +50,20 @@ "local_ip": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 `0.0.0.0` \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." } }, + "manual_tunnel": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u043d\u0430 Home Assistant", + "port": "\u041f\u043e\u0440\u0442" + } + }, "tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442", "tunneling_type": "KNX \u0442\u0443\u043d\u0435\u043b\u0435\u043d \u0442\u0438\u043f" - } + }, + "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0448\u043b\u044e\u0437 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430." } } } diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 596071695b4c34..f6aaca60d18d2f 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -9,20 +9,30 @@ "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "invalid_individual_address": "Verdien samsvarer ikke med m\u00f8nsteret for individuelle KNX-adresser.\n 'area.line.device'", "invalid_ip_address": "Ugyldig IPv4-adresse.", - "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil." + "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", + "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX tilkoblingstype" + }, + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + }, "manual_tunnel": { "data": { "host": "Vert", "local_ip": "Lokal IP for hjemmeassistent", "port": "Port", + "route_back": "Rute tilbake / NAT-modus", "tunneling_type": "KNX tunneltype" }, "data_description": { "host": "IP-adressen til KNX/IP-tunnelenheten.", "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse.", - "port": "Port p\u00e5 KNX/IP-tunnelenheten." + "port": "Port p\u00e5 KNX/IP-tunnelenheten.", + "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." }, "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." }, @@ -63,11 +73,25 @@ }, "description": "Vennligst skriv inn din sikre IP-informasjon." }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Enhetsautentiseringspassord", + "user_id": "bruker-ID", + "user_password": "Bruker passord" + }, + "data_description": { + "device_authentication": "Dette settes i 'IP'-panelet til grensesnittet i ETS.", + "user_id": "Dette er ofte tunnelnummer +1. S\u00e5 'Tunnel 2' ville ha bruker-ID '3'.", + "user_password": "Passord for den spesifikke tunnelforbindelsen satt i 'Egenskaper'-panelet i tunnelen i ETS." + }, + "description": "Vennligst skriv inn din sikre IP-informasjon." + }, "secure_tunneling": { "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", "menu_options": { "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder IP-sikre n\u00f8kler", - "secure_manual": "Konfigurer IP-sikre n\u00f8kler manuelt" + "secure_manual": "Konfigurer IP-sikre n\u00f8kler manuelt", + "secure_tunnel_manual": "Konfigurer IP-sikre n\u00f8kler manuelt" } }, "tunnel": { @@ -85,7 +109,32 @@ } }, "options": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", + "invalid_individual_address": "Verdien samsvarer ikke med m\u00f8nsteret for individuelle KNX-adresser.\n 'area.line.device'", + "invalid_ip_address": "Ugyldig IPv4-adresse.", + "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", + "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt." + }, "step": { + "communication_settings": { + "data": { + "rate_limit": "Satsgrense", + "state_updater": "Statens oppdatering" + }, + "data_description": { + "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund.\n `0` for \u00e5 deaktivere grensen. Anbefalt: 0 eller 20 til 40", + "state_updater": "Angi standard for lesing av tilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente enhetstilstander fra KNX-bussen. Kan overstyres av entitetsalternativer for \"sync_state\"." + } + }, + "connection_type": { + "data": { + "connection_type": "KNX tilkoblingstype" + }, + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + }, "init": { "data": { "connection_type": "KNX tilkoblingstype", @@ -105,8 +154,75 @@ "state_updater": "Sett standard for lesing av tilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente enhetstilstander fra KNX-bussen. Kan overstyres av entitetsalternativer for \"sync_state\"." } }, + "manual_tunnel": { + "data": { + "host": "Vert", + "local_ip": "Lokal IP for hjemmeassistent", + "port": "Port", + "route_back": "Rute tilbake / NAT-modus", + "tunneling_type": "KNX tunneltype" + }, + "data_description": { + "host": "IP-adressen til KNX/IP-tunnelenheten.", + "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse.", + "port": "Port p\u00e5 KNX/IP-tunnelenheten.", + "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." + }, + "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." + }, + "options_init": { + "menu_options": { + "communication_settings": "Kommunikasjonsinnstillinger", + "connection_type": "Konfigurer KNX-grensesnitt" + } + }, + "routing": { + "data": { + "individual_address": "Individuell adresse", + "local_ip": "Lokal IP for hjemmeassistent", + "multicast_group": "Multicast gruppe", + "multicast_port": "Multicast port" + }, + "data_description": { + "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", + "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse." + }, + "description": "Vennligst konfigurer rutealternativene." + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Filnavnet til `.knxkeys`-filen (inkludert utvidelse)", + "knxkeys_password": "Passordet for \u00e5 dekryptere `.knxkeys`-filen" + }, + "data_description": { + "knxkeys_filename": "Filen forventes \u00e5 bli funnet i konfigurasjonskatalogen din i `.storage/knx/`.\n I Home Assistant OS vil dette v\u00e6re `/config/.storage/knx/`\n Eksempel: `mitt_prosjekt.knxkeys`", + "knxkeys_password": "Dette ble satt ved eksport av filen fra ETS." + }, + "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen." + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Enhetsautentiseringspassord", + "user_id": "bruker-ID", + "user_password": "Bruker passord" + }, + "data_description": { + "device_authentication": "Dette settes i 'IP'-panelet til grensesnittet i ETS.", + "user_id": "Dette er ofte tunnelnummer +1. S\u00e5 'Tunnel 2' ville ha bruker-ID '3'.", + "user_password": "Passord for den spesifikke tunnelforbindelsen satt i 'Egenskaper'-panelet i tunnelen i ETS." + }, + "description": "Vennligst skriv inn din sikre IP-informasjon." + }, + "secure_tunneling": { + "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder IP-sikre n\u00f8kler", + "secure_tunnel_manual": "Konfigurer IP-sikre n\u00f8kler manuelt" + } + }, "tunnel": { "data": { + "gateway": "KNX Tunneltilkobling", "host": "Vert", "port": "Port", "tunneling_type": "KNX tunneltype" @@ -114,7 +230,8 @@ "data_description": { "host": "IP-adressen til KNX/IP-tunnelenheten.", "port": "Port p\u00e5 KNX/IP-tunnelenheten." - } + }, + "description": "Vennligst velg en gateway fra listen." } } } diff --git a/homeassistant/components/livisi/translations/ca.json b/homeassistant/components/livisi/translations/ca.json index 359b7eb01b1b4a..e765c9199e008d 100644 --- a/homeassistant/components/livisi/translations/ca.json +++ b/homeassistant/components/livisi/translations/ca.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "wrong_ip_address": "L'adre\u00e7a IP \u00e9s incorrecta o no es pot connectar amb l'SHC localment.", "wrong_password": "La contrasenya \u00e9s incorrecta." }, "step": { @@ -9,7 +10,8 @@ "data": { "host": "Adre\u00e7a IP", "password": "Contrasenya" - } + }, + "description": "Introdueix l'adre\u00e7a IP i la contrasenya (local) de l'SHC." } } } diff --git a/homeassistant/components/mjpeg/translations/bg.json b/homeassistant/components/mjpeg/translations/bg.json index 0e88f5081913d5..19b62c42896142 100644 --- a/homeassistant/components/mjpeg/translations/bg.json +++ b/homeassistant/components/mjpeg/translations/bg.json @@ -13,6 +13,7 @@ "mjpeg_url": "MJPEG URL", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "still_image_url": "URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } @@ -30,6 +31,7 @@ "mjpeg_url": "MJPEG URL", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "still_image_url": "URL \u043d\u0430 \u043d\u0435\u043f\u043e\u0434\u0432\u0438\u0436\u043d\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } } diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 13c91abe856919..7dc484cdcd4562 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -20,10 +20,10 @@ "data": { "advanced_options": "Opcions avan\u00e7ades", "broker": "Broker", - "certificate": "Ruta a un fitxer de certificat CA personalitzat", - "client_cert": "Ruta a un fitxer de certificat de client", + "certificate": "Puja fitxer de certificat CA personalitzat", + "client_cert": "Puja fitxer de certificat client", "client_id": "ID de client (deixa-ho buit per generar-lo aleat\u00f2riament)", - "client_key": "Ruta a un fitxer de clau privada", + "client_key": "Puja fitxer de clau privada", "discovery": "Habilita el descobriment autom\u00e0tic", "keepalive": "Temps entre enviaments de missatges de manteniment viu ('keep alive')", "password": "Contrasenya", @@ -38,7 +38,7 @@ }, "hassio_confirm": { "data": { - "discovery": "Habilitar descobriment autom\u00e0tic" + "discovery": "Activa el descobriment" }, "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement {addon}?", "title": "Broker MQTT via complement de Home Assistant" @@ -94,10 +94,10 @@ "data": { "advanced_options": "Opcions avan\u00e7ades", "broker": "Broker", - "certificate": "Puja un fitxer de certificat CA personalitzat", - "client_cert": "Puja un fitxer de certificat de client", + "certificate": "Puja fitxer de certificat CA personalitzat", + "client_cert": "Puja fitxer de certificat client", "client_id": "ID de client (deixa-ho buit per generar-lo aleat\u00f2riament)", - "client_key": "Puja un fitxer de clau privada", + "client_key": "Puja fitxer de clau privada", "keepalive": "Temps entre enviaments de missatges de manteniment viu ('keep alive')", "password": "Contrasenya", "port": "Port", @@ -117,7 +117,7 @@ "birth_qos": "QoS del missatge de naixement", "birth_retain": "Retenci\u00f3 del missatge de naixement", "birth_topic": "Topic del missatge de naixement", - "discovery": "Activar descobriment", + "discovery": "Activa el descobriment", "discovery_prefix": "Prefix de descobriment", "will_enable": "Activa el missatge d'\u00faltima voluntat", "will_payload": "Dades (payload) del missatge d'\u00faltima voluntat", diff --git a/homeassistant/components/nibe_heatpump/translations/ca.json b/homeassistant/components/nibe_heatpump/translations/ca.json index de5814b4f38998..a8a91740a7d73d 100644 --- a/homeassistant/components/nibe_heatpump/translations/ca.json +++ b/homeassistant/components/nibe_heatpump/translations/ca.json @@ -6,16 +6,22 @@ "error": { "address": "Adre\u00e7a remota inv\u00e0lida. L'adre\u00e7a ha de ser una adre\u00e7a IP o un nom d'amfitri\u00f3 resoluble.", "address_in_use": "El port d'escolta seleccionat ja est\u00e0 en \u00fas en aquest sistema.", - "model": "El model seleccionat no sembla admetre modbus40", - "read": "Error en la sol\u00b7licitud de lectura de la bomba. Verifica el port remot de lectura i/o l'adre\u00e7a IP remota.", + "model": "El model seleccionat no sembla admetre MODBUS40", + "read": "Error en la sol\u00b7licitud de lectura de la bomba. Verifica el `port remot de lectura` i/o `l'adre\u00e7a remota`.", "unknown": "Error inesperat", - "write": "Error en la sol\u00b7licitud d'escriptura a la bomba. Verifica el port remot d'escriptura i/o l'adre\u00e7a IP remota." + "url": "L'URL especificat no est\u00e0 ben format o no \u00e9s compatible", + "write": "Error en la sol\u00b7licitud d'escriptura a la bomba. Verifica el `port remot d'escriptura` i/o `l'adre\u00e7a remota`." }, "step": { "modbus": { "data": { + "modbus_unit": "Identificador d'unitat Modbus", "modbus_url": "URL de Modbus", "model": "Model de bomba de calor" + }, + "data_description": { + "modbus_unit": "Identificaci\u00f3 de la teva bomba de calor. Normalment es pot deixar a 0.", + "modbus_url": "URL de Modbus que descriu la connexi\u00f3 a la teva bomba de calor o unitat MODBUS40. Ha d'estar en el format:\n - `tcp://[HOST]:[PORT]` per a una connexi\u00f3 Modbus TCP\n - `serial://[DISPOSITIU LOCAL]` per a una connexi\u00f3 Modbus RTU local\n - `rfc2217://[HOST]:[PORT]` per a una connexi\u00f3 remota Modbus RTU basada en telnet." } }, "nibegw": { @@ -47,7 +53,7 @@ "remote_read_port": "Port on la unitat NibeGW espera les sol\u00b7licituds de lectura.", "remote_write_port": "Port on la unitat NibeGW espera les sol\u00b7licituds d'escriptura." }, - "description": "Abans d'intentar configurar la integraci\u00f3, comprova que:\n - La unitat NibeGW est\u00e0 connectada a una bomba de calor.\n - S'ha activat l'accessori MODBUS40 a la configuraci\u00f3 de la bomba de calor.\n - La bomba no ha entrat en estat d'alarma per falta de l'accessori MODBUS40.", + "description": "Tria el m\u00e8tode de connexi\u00f3 a la teva bomba. En general, les bombes de la s\u00e8rie F necessiten un accessori personalitzat NibeGW, mentre que les bombes de la s\u00e8rie S tenen Modbus integrat.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/nibe_heatpump/translations/no.json b/homeassistant/components/nibe_heatpump/translations/no.json index e2ffa8790f7531..d7da2189116b38 100644 --- a/homeassistant/components/nibe_heatpump/translations/no.json +++ b/homeassistant/components/nibe_heatpump/translations/no.json @@ -6,11 +6,11 @@ "error": { "address": "Ugyldig ekstern adresse er angitt. Adressen m\u00e5 v\u00e6re en IP-adresse eller et vertsnavn som kan l\u00f8ses.", "address_in_use": "Den valgte lytteporten er allerede i bruk p\u00e5 dette systemet.", - "model": "Den valgte modellen ser ikke ut til \u00e5 st\u00f8tte modbus40", - "read": "Feil ved leseforesp\u00f8rsel fra pumpe. Bekreft din \"Ekstern leseport\" eller \"Ekstern IP-adresse\".", + "model": "Den valgte modellen ser ikke ut til \u00e5 st\u00f8tte MODBUS40", + "read": "Feil ved leseforesp\u00f8rsel fra pumpe. Bekreft din \"Ekstern leseport\" eller \"Ekstern adresse\".", "unknown": "Uventet feil", - "url": "Den spesifiserte nettadressen er ikke en godt utformet og st\u00f8ttet nettadresse", - "write": "Feil ved skriveforesp\u00f8rsel til pumpen. Bekreft din \"Ekstern skriveport\" eller \"Ekstern IP-adresse\"." + "url": "Den angitte URL-en er ikke godt utformet eller st\u00f8ttet", + "write": "Feil ved skriveforesp\u00f8rsel til pumpen. Bekreft din \"Ekstern skriveport\" eller \"Ekstern adresse\"." }, "step": { "modbus": { @@ -53,7 +53,7 @@ "remote_read_port": "Porten NibeGW-enheten lytter etter leseforesp\u00f8rsler p\u00e5.", "remote_write_port": "Porten NibeGW-enheten lytter etter skriveforesp\u00f8rsler p\u00e5." }, - "description": "Velg tilkoblingsmetoden til pumpen din. Generelt krever pumper i F-serien et Nibe GW-tilpasset tilbeh\u00f8r, mens en pumpe i S-serien har Modbus-st\u00f8tte innebygd.", + "description": "Velg tilkoblingsmetoden til pumpen din. Generelt krever pumper i F-serien et NibeGW-tilpasset tilbeh\u00f8r, mens en pumpe i S-serien har Modbus-st\u00f8tte innebygd.", "menu_options": { "modbus": "Modbus", "nibegw": "NibeGW" diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 235e05bb78a7d4..f982fcc9dd215d 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -29,6 +29,19 @@ } } }, + "issues": { + "deprecated_key": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei. S'han de substituir totes les claus o entrades anomenades 'name' per 'entry_id'.", + "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" + } + } + }, + "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" + } + }, "options": { "step": { "init": { From 1fe85c9b1700f36d41374490701c9f8738137273 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 12 Nov 2022 10:43:11 +0100 Subject: [PATCH 387/394] Fix accelerator sensor in fibaro integration (#81237) * Fix accelerator sensor in fibaro integration * Implement suggestions from code review * Implement suggestions from code review * Changes as suggested in code review * Adjust as suggested in code review --- homeassistant/components/fibaro/__init__.py | 1 + .../components/fibaro/binary_sensor.py | 56 ++++++++++++++++--- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 3661721810b084..19f367427409bd 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -84,6 +84,7 @@ "com.fibaro.thermostatDanfoss": Platform.CLIMATE, "com.fibaro.doorLock": Platform.LOCK, "com.fibaro.binarySensor": Platform.BINARY_SENSOR, + "com.fibaro.accelerometer": Platform.BINARY_SENSOR, } DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 359869efc25219..f9baa33c41f5a8 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Fibaro binary sensors.""" from __future__ import annotations +from collections.abc import Mapping +import json from typing import Any from homeassistant.components.binary_sensor import ( @@ -28,6 +30,11 @@ "com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", BinarySensorDeviceClass.SMOKE], "com.fibaro.FGMS001": ["Motion", "mdi:run", BinarySensorDeviceClass.MOTION], "com.fibaro.heatDetector": ["Heat", "mdi:fire", BinarySensorDeviceClass.HEAT], + "com.fibaro.accelerometer": [ + "Moving", + "mdi:axis-arrow", + BinarySensorDeviceClass.MOVING, + ], } @@ -55,15 +62,50 @@ def __init__(self, fibaro_device: Any) -> None: """Initialize the binary_sensor.""" super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - stype = None + self._own_extra_state_attributes: Mapping[str, Any] = {} + self._fibaro_sensor_type = None if fibaro_device.type in SENSOR_TYPES: - stype = fibaro_device.type + self._fibaro_sensor_type = fibaro_device.type elif fibaro_device.baseType in SENSOR_TYPES: - stype = fibaro_device.baseType - if stype: - self._attr_device_class = SENSOR_TYPES[stype][2] - self._attr_icon = SENSOR_TYPES[stype][1] + self._fibaro_sensor_type = fibaro_device.baseType + if self._fibaro_sensor_type: + self._attr_device_class = SENSOR_TYPES[self._fibaro_sensor_type][2] + self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1] + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return the extra state attributes of the device.""" + return super().extra_state_attributes | self._own_extra_state_attributes def update(self) -> None: """Get the latest data and update the state.""" - self._attr_is_on = self.current_binary_state + if self._fibaro_sensor_type == "com.fibaro.accelerometer": + # Accelerator sensors have values for the three axis x, y and z + moving_values = self._get_moving_values() + self._attr_is_on = self._is_moving(moving_values) + self._own_extra_state_attributes = self._get_xyz_moving(moving_values) + else: + self._attr_is_on = self.current_binary_state + + def _get_xyz_moving(self, moving_values: Mapping[str, Any]) -> Mapping[str, Any]: + """Return x y z values of the accelerator sensor value.""" + attrs = {} + for axis_name in ("x", "y", "z"): + attrs[axis_name] = float(moving_values[axis_name]) + return attrs + + def _is_moving(self, moving_values: Mapping[str, Any]) -> bool: + """Return that a moving is detected when one axis reports a value.""" + for axis_name in ("x", "y", "z"): + if float(moving_values[axis_name]) != 0: + return True + return False + + def _get_moving_values(self) -> Mapping[str, Any]: + """Get the moving values of the accelerator sensor in a dict.""" + value = self.fibaro_device.properties.value + if isinstance(value, str): + # HC2 returns dict as str + return json.loads(value) + # HC3 returns a real dict + return value From ee910bd0e41391e00ccd521fe7d605e494d33046 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 13 Nov 2022 01:22:59 +0800 Subject: [PATCH 388/394] Refactor camera stream settings (#81663) --- homeassistant/components/camera/__init__.py | 23 ++--- homeassistant/components/camera/prefs.py | 86 +++++++++---------- .../components/generic/config_flow.py | 14 ++- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/stream/__init__.py | 41 +++++---- homeassistant/components/stream/core.py | 19 ++-- homeassistant/components/stream/hls.py | 13 ++- homeassistant/components/stream/recorder.py | 7 +- tests/components/camera/test_init.py | 18 ++-- tests/components/stream/common.py | 12 ++- tests/components/stream/test_hls.py | 31 +++---- tests/components/stream/test_ll_hls.py | 21 +++-- tests/components/stream/test_recorder.py | 27 +++--- tests/components/stream/test_worker.py | 66 ++++++++++---- 14 files changed, 227 insertions(+), 153 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index da88dc49a5b1cb..82d981fc0cc9e4 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -5,7 +5,7 @@ import collections from collections.abc import Awaitable, Callable, Iterable from contextlib import suppress -from dataclasses import dataclass +from dataclasses import asdict, dataclass from datetime import datetime, timedelta from enum import IntEnum from functools import partial @@ -74,7 +74,7 @@ StreamType, ) from .img_util import scale_jpeg_camera_image -from .prefs import CameraPreferences +from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401 _LOGGER = logging.getLogger(__name__) @@ -346,7 +346,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) prefs = CameraPreferences(hass) - await prefs.async_initialize() hass.data[DATA_CAMERA_PREFS] = prefs hass.http.register_view(CameraImageView(component)) @@ -361,13 +360,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def preload_stream(_event: Event) -> None: for camera in component.entities: - camera_prefs = prefs.get(camera.entity_id) - if not camera_prefs.preload_stream: + stream_prefs = await prefs.get_dynamic_stream_settings(camera.entity_id) + if not stream_prefs.preload_stream: continue stream = await camera.async_create_stream() if not stream: continue - stream.keepalive = True stream.add_provider("hls") await stream.start() @@ -524,6 +522,9 @@ async def async_create_stream(self) -> Stream | None: self.hass, source, options=self.stream_options, + dynamic_stream_settings=await self.hass.data[ + DATA_CAMERA_PREFS + ].get_dynamic_stream_settings(self.entity_id), stream_label=self.entity_id, ) self.stream.set_update_callback(self.async_write_ha_state) @@ -861,8 +862,8 @@ async def websocket_get_prefs( ) -> None: """Handle request for account info.""" prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS] - camera_prefs = prefs.get(msg["entity_id"]) - connection.send_result(msg["id"], camera_prefs.as_dict()) + stream_prefs = await prefs.get_dynamic_stream_settings(msg["entity_id"]) + connection.send_result(msg["id"], asdict(stream_prefs)) @websocket_api.websocket_command( @@ -956,12 +957,6 @@ async def _async_stream_endpoint_url( f"{camera.entity_id} does not support play stream service" ) - # Update keepalive setting which manages idle shutdown - prefs: CameraPreferences = hass.data[DATA_CAMERA_PREFS] - camera_prefs = prefs.get(camera.entity_id) - stream.keepalive = camera_prefs.preload_stream - stream.orientation = camera_prefs.orientation - stream.add_provider(fmt) await stream.start() return stream.endpoint_url(fmt) diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index fac93df474ee40..0a8785457e8713 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,6 +1,7 @@ """Preference management for camera component.""" from __future__ import annotations +from dataclasses import asdict, dataclass from typing import Final, Union, cast from homeassistant.components.stream import Orientation @@ -16,28 +17,12 @@ STORAGE_VERSION: Final = 1 -class CameraEntityPreferences: - """Handle preferences for camera entity.""" +@dataclass +class DynamicStreamSettings: + """Stream settings which are managed and updated by the camera entity.""" - def __init__(self, prefs: dict[str, bool | Orientation]) -> None: - """Initialize prefs.""" - self._prefs = prefs - - def as_dict(self) -> dict[str, bool | Orientation]: - """Return dictionary version.""" - return self._prefs - - @property - def preload_stream(self) -> bool: - """Return if stream is loaded on hass start.""" - return cast(bool, self._prefs.get(PREF_PRELOAD_STREAM, False)) - - @property - def orientation(self) -> Orientation: - """Return the current stream orientation settings.""" - return cast( - Orientation, self._prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM) - ) + preload_stream: bool = False + orientation: Orientation = Orientation.NO_TRANSFORM class CameraPreferences: @@ -51,15 +36,9 @@ def __init__(self, hass: HomeAssistant) -> None: self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]]( hass, STORAGE_VERSION, STORAGE_KEY ) - # Local copy of the preload_stream prefs - self._prefs: dict[str, dict[str, bool | Orientation]] | None = None - - async def async_initialize(self) -> None: - """Finish initializing the preferences.""" - if (prefs := await self._store.async_load()) is None: - prefs = {} - - self._prefs = prefs + self._dynamic_stream_settings_by_entity_id: dict[ + str, DynamicStreamSettings + ] = {} async def async_update( self, @@ -67,20 +46,25 @@ async def async_update( *, preload_stream: bool | UndefinedType = UNDEFINED, orientation: Orientation | UndefinedType = UNDEFINED, - stream_options: dict[str, str] | UndefinedType = UNDEFINED, ) -> dict[str, bool | Orientation]: """Update camera preferences. + Also update the DynamicStreamSettings if they exist. + preload_stream is stored in a Store + orientation is stored in the Entity Registry + Returns a dict with the preferences on success. Raises HomeAssistantError on failure. """ + dynamic_stream_settings = self._dynamic_stream_settings_by_entity_id.get( + entity_id + ) if preload_stream is not UNDEFINED: - # Prefs already initialized. - assert self._prefs is not None - if not self._prefs.get(entity_id): - self._prefs[entity_id] = {} - self._prefs[entity_id][PREF_PRELOAD_STREAM] = preload_stream - await self._store.async_save(self._prefs) + if dynamic_stream_settings: + dynamic_stream_settings.preload_stream = preload_stream + preload_prefs = await self._store.async_load() or {} + preload_prefs[entity_id] = {PREF_PRELOAD_STREAM: preload_stream} + await self._store.async_save(preload_prefs) if orientation is not UNDEFINED: if (registry := er.async_get(self._hass)).async_get(entity_id): @@ -91,12 +75,26 @@ async def async_update( raise HomeAssistantError( "Orientation is only supported on entities set up through config flows" ) - return self.get(entity_id).as_dict() - - def get(self, entity_id: str) -> CameraEntityPreferences: - """Get preferences for an entity.""" - # Prefs are already initialized. - assert self._prefs is not None + if dynamic_stream_settings: + dynamic_stream_settings.orientation = orientation + return asdict(await self.get_dynamic_stream_settings(entity_id)) + + async def get_dynamic_stream_settings( + self, entity_id: str + ) -> DynamicStreamSettings: + """Get the DynamicStreamSettings for the entity.""" + if settings := self._dynamic_stream_settings_by_entity_id.get(entity_id): + return settings + # Get preload stream setting from prefs + # Get orientation setting from entity registry reg_entry = er.async_get(self._hass).async_get(entity_id) er_prefs = reg_entry.options.get(DOMAIN, {}) if reg_entry else {} - return CameraEntityPreferences(self._prefs.get(entity_id, {}) | er_prefs) + preload_prefs = await self._store.async_load() or {} + settings = DynamicStreamSettings( + preload_stream=cast( + bool, preload_prefs.get(entity_id, {}).get(PREF_PRELOAD_STREAM, False) + ), + orientation=er_prefs.get(PREF_ORIENTATION, Orientation.NO_TRANSFORM), + ) + self._dynamic_stream_settings_by_entity_id[entity_id] = settings + return settings diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 09f52705734a44..6fa01ba369efad 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -16,7 +16,11 @@ import voluptuous as vol import yarl -from homeassistant.components.camera import CAMERA_IMAGE_TIMEOUT, _async_get_image +from homeassistant.components.camera import ( + CAMERA_IMAGE_TIMEOUT, + DynamicStreamSettings, + _async_get_image, +) from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, @@ -246,7 +250,13 @@ async def async_test_stream( url = url.with_user(username).with_password(password) stream_source = str(url) try: - stream = create_stream(hass, stream_source, stream_options, "test_stream") + stream = create_stream( + hass, + stream_source, + stream_options, + DynamicStreamSettings(), + "test_stream", + ) hls_provider = stream.add_provider(HLS_PROVIDER) await stream.start() if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT): diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 9a8535f2599783..11699731b2f14d 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -137,7 +137,7 @@ async def async_camera_image( ) -> bytes | None: """Return a still image response from the camera.""" - if self.stream and self.stream.keepalive: + if self.stream and self.stream.dynamic_stream_settings.preload_stream: return await self.stream.async_get_image(width, height) if self.device.capabilities.snapshot: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 01cd3c2962cb0c..7e6f663fc288b2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -25,7 +25,7 @@ import threading import time from types import MappingProxyType -from typing import Any, Final, cast +from typing import TYPE_CHECKING, Any, Final, cast import voluptuous as vol @@ -70,6 +70,9 @@ from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls +if TYPE_CHECKING: + from homeassistant.components.camera import DynamicStreamSettings + __all__ = [ "ATTR_SETTINGS", "CONF_EXTRA_PART_WAIT_TIME", @@ -105,6 +108,7 @@ def create_stream( hass: HomeAssistant, stream_source: str, options: Mapping[str, str | bool | float], + dynamic_stream_settings: DynamicStreamSettings, stream_label: str | None = None, ) -> Stream: """Create a stream with the specified identfier based on the source url. @@ -156,6 +160,7 @@ def convert_stream_options( stream_source, pyav_options=pyav_options, stream_settings=stream_settings, + dynamic_stream_settings=dynamic_stream_settings, stream_label=stream_label, ) hass.data[DOMAIN][ATTR_STREAMS].append(stream) @@ -231,7 +236,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: part_target_duration=conf[CONF_PART_DURATION], hls_advance_part_limit=max(int(3 / conf[CONF_PART_DURATION]), 3), hls_part_timeout=2 * conf[CONF_PART_DURATION], - orientation=Orientation.NO_TRANSFORM, ) else: hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS @@ -246,7 +250,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def shutdown(event: Event) -> None: """Stop all stream workers.""" for stream in hass.data[DOMAIN][ATTR_STREAMS]: - stream.keepalive = False + stream.dynamic_stream_settings.preload_stream = False if awaitables := [ asyncio.create_task(stream.stop()) for stream in hass.data[DOMAIN][ATTR_STREAMS] @@ -268,6 +272,7 @@ def __init__( source: str, pyav_options: dict[str, str], stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, stream_label: str | None = None, ) -> None: """Initialize a stream.""" @@ -276,14 +281,16 @@ def __init__( self.pyav_options = pyav_options self._stream_settings = stream_settings self._stream_label = stream_label - self.keepalive = False + self.dynamic_stream_settings = dynamic_stream_settings self.access_token: str | None = None self._start_stop_lock = asyncio.Lock() self._thread: threading.Thread | None = None self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} self._fast_restart_once = False - self._keyframe_converter = KeyFrameConverter(hass, stream_settings) + self._keyframe_converter = KeyFrameConverter( + hass, stream_settings, dynamic_stream_settings + ) self._available: bool = True self._update_callback: Callable[[], None] | None = None self._logger = ( @@ -293,16 +300,6 @@ def __init__( ) self._diagnostics = Diagnostics() - @property - def orientation(self) -> Orientation: - """Return the current orientation setting.""" - return self._stream_settings.orientation - - @orientation.setter - def orientation(self, value: Orientation) -> None: - """Set the stream orientation setting.""" - self._stream_settings.orientation = value - def endpoint_url(self, fmt: str) -> str: """Start the stream and returns a url for the output format.""" if fmt not in self._outputs: @@ -326,7 +323,8 @@ def add_provider( async def idle_callback() -> None: if ( - not self.keepalive or fmt == RECORDER_PROVIDER + not self.dynamic_stream_settings.preload_stream + or fmt == RECORDER_PROVIDER ) and fmt in self._outputs: await self.remove_provider(self._outputs[fmt]) self.check_idle() @@ -335,6 +333,7 @@ async def idle_callback() -> None: self.hass, IdleTimer(self.hass, timeout, idle_callback), self._stream_settings, + self.dynamic_stream_settings, ) self._outputs[fmt] = provider @@ -413,8 +412,12 @@ def _run_worker(self) -> None: while not self._thread_quit.wait(timeout=wait_timeout): start_time = time.time() self.hass.add_job(self._async_update_state, True) - self._diagnostics.set_value("keepalive", self.keepalive) - self._diagnostics.set_value("orientation", self.orientation) + self._diagnostics.set_value( + "keepalive", self.dynamic_stream_settings.preload_stream + ) + self._diagnostics.set_value( + "orientation", self.dynamic_stream_settings.orientation + ) self._diagnostics.increment("start_worker") try: stream_worker( @@ -473,7 +476,7 @@ async def stop(self) -> None: self._outputs = {} self.access_token = None - if not self.keepalive: + if not self.dynamic_stream_settings.preload_stream: await self._stop() async def _stop(self) -> None: diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 31654f7d0dbbde..a21a9f17d969ca 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -29,6 +29,8 @@ if TYPE_CHECKING: from av import CodecContext, Packet + from homeassistant.components.camera import DynamicStreamSettings + from . import Stream _LOGGER = logging.getLogger(__name__) @@ -58,7 +60,6 @@ class StreamSettings: part_target_duration: float = attr.ib() hls_advance_part_limit: int = attr.ib() hls_part_timeout: float = attr.ib() - orientation: Orientation = attr.ib() STREAM_SETTINGS_NON_LL_HLS = StreamSettings( @@ -67,7 +68,6 @@ class StreamSettings: part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, hls_advance_part_limit=3, hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - orientation=Orientation.NO_TRANSFORM, ) @@ -273,12 +273,14 @@ def __init__( hass: HomeAssistant, idle_timer: IdleTimer, stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, deque_maxlen: int | None = None, ) -> None: """Initialize a stream output.""" self._hass = hass self.idle_timer = idle_timer self.stream_settings = stream_settings + self.dynamic_stream_settings = dynamic_stream_settings self._event = asyncio.Event() self._part_event = asyncio.Event() self._segments: deque[Segment] = deque(maxlen=deque_maxlen) @@ -427,7 +429,12 @@ class KeyFrameConverter: If unsuccessful, get_image will return the previous image """ - def __init__(self, hass: HomeAssistant, stream_settings: StreamSettings) -> None: + def __init__( + self, + hass: HomeAssistant, + stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, + ) -> None: """Initialize.""" # Keep import here so that we can import stream integration without installing reqs @@ -441,6 +448,7 @@ def __init__(self, hass: HomeAssistant, stream_settings: StreamSettings) -> None self._lock = asyncio.Lock() self._codec_context: CodecContext | None = None self._stream_settings = stream_settings + self._dynamic_stream_settings = dynamic_stream_settings def create_codec_context(self, codec_context: CodecContext) -> None: """ @@ -498,12 +506,13 @@ def _generate_image(self, width: int | None, height: int | None) -> None: if frames: frame = frames[0] if width and height: - if self._stream_settings.orientation >= 5: + if self._dynamic_stream_settings.orientation >= 5: frame = frame.reformat(width=height, height=width) else: frame = frame.reformat(width=width, height=height) bgr_array = self.transform_image( - frame.to_ndarray(format="bgr24"), self._stream_settings.orientation + frame.to_ndarray(format="bgr24"), + self._dynamic_stream_settings.orientation, ) self._image = bytes(self._turbojpeg.encode(bgr_array)) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index e8920abcaa60a5..cddb4413ed8a49 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -27,6 +27,8 @@ from .fmp4utils import get_codec_string, transform_init if TYPE_CHECKING: + from homeassistant.components.camera import DynamicStreamSettings + from . import Stream @@ -50,9 +52,16 @@ def __init__( hass: HomeAssistant, idle_timer: IdleTimer, stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, ) -> None: """Initialize HLS output.""" - super().__init__(hass, idle_timer, stream_settings, deque_maxlen=MAX_SEGMENTS) + super().__init__( + hass, + idle_timer, + stream_settings, + dynamic_stream_settings, + deque_maxlen=MAX_SEGMENTS, + ) self._target_duration = stream_settings.min_segment_duration @property @@ -339,7 +348,7 @@ async def handle( if not (segments := track.get_segments()) or not (body := segments[0].init): return web.HTTPNotFound() return web.Response( - body=transform_init(body, stream.orientation), + body=transform_init(body, stream.dynamic_stream_settings.orientation), headers={"Content-Type": "video/mp4"}, ) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index e917292251a288..fffbd489757fa1 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -21,6 +21,8 @@ if TYPE_CHECKING: import deque + from homeassistant.components.camera import DynamicStreamSettings + _LOGGER = logging.getLogger(__name__) @@ -38,9 +40,10 @@ def __init__( hass: HomeAssistant, idle_timer: IdleTimer, stream_settings: StreamSettings, + dynamic_stream_settings: DynamicStreamSettings, ) -> None: """Initialize recorder output.""" - super().__init__(hass, idle_timer, stream_settings) + super().__init__(hass, idle_timer, stream_settings, dynamic_stream_settings) self.video_path: str @property @@ -154,7 +157,7 @@ def write_transform_matrix_and_rename(video_path: str) -> None: video_path, mode="wb" ) as out_file: init = transform_init( - read_init(in_file), self.stream_settings.orientation + read_init(in_file), self.dynamic_stream_settings.orientation ) out_file.write(init) in_file.seek(len(init)) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 9bf35ec55fa33f..3a78740684ae74 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -12,7 +12,6 @@ PREF_ORIENTATION, PREF_PRELOAD_STREAM, ) -from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( @@ -302,8 +301,9 @@ async def test_websocket_update_preload_prefs(hass, hass_ws_client, mock_camera) ) msg = await client.receive_json() - # There should be no preferences - assert not msg["result"] + # The default prefs should be returned. Preload stream should be False + assert msg["success"] + assert msg["result"][PREF_PRELOAD_STREAM] is False # Update the preference await client.send_json( @@ -421,12 +421,12 @@ async def test_handle_play_stream_service(hass, mock_camera, mock_stream): async def test_no_preload_stream(hass, mock_stream): """Test camera preload preference.""" - demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: False}) + demo_settings = camera.DynamicStreamSettings() with patch( "homeassistant.components.camera.Stream.endpoint_url", ) as mock_request_stream, patch( - "homeassistant.components.camera.prefs.CameraPreferences.get", - return_value=demo_prefs, + "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", + return_value=demo_settings, ), patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", new_callable=PropertyMock, @@ -440,12 +440,12 @@ async def test_no_preload_stream(hass, mock_stream): async def test_preload_stream(hass, mock_stream): """Test camera preload preference.""" - demo_prefs = CameraEntityPreferences({PREF_PRELOAD_STREAM: True}) + demo_settings = camera.DynamicStreamSettings(preload_stream=True) with patch( "homeassistant.components.camera.create_stream" ) as mock_create_stream, patch( - "homeassistant.components.camera.prefs.CameraPreferences.get", - return_value=demo_prefs, + "homeassistant.components.camera.prefs.CameraPreferences.get_dynamic_stream_settings", + return_value=demo_settings, ), patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="http://example.com", diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index de5b2c234ebf9b..ff98d90ea8d55c 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -8,7 +8,8 @@ import av import numpy as np -from homeassistant.components.stream.core import Segment +from homeassistant.components.camera import DynamicStreamSettings +from homeassistant.components.stream.core import Orientation, Segment from homeassistant.components.stream.fmp4utils import ( TRANSFORM_MATRIX_TOP, XYW_ROW, @@ -16,8 +17,8 @@ ) FAKE_TIME = datetime.utcnow() -# Segment with defaults filled in for use in tests +# Segment with defaults filled in for use in tests DefaultSegment = partial( Segment, init=None, @@ -157,7 +158,7 @@ def remux_with_audio(source, container_format, audio_codec): return output -def assert_mp4_has_transform_matrix(mp4: bytes, orientation: int): +def assert_mp4_has_transform_matrix(mp4: bytes, orientation: Orientation): """Assert that the mp4 (or init) has the proper transformation matrix.""" # Find moov moov_location = next(find_box(mp4, b"moov")) @@ -170,3 +171,8 @@ def assert_mp4_has_transform_matrix(mp4: bytes, orientation: int): mp4[tkhd_location + tkhd_length - 44 : tkhd_location + tkhd_length - 8] == TRANSFORM_MATRIX_TOP[orientation] + XYW_ROW ) + + +def dynamic_stream_settings(): + """Create new dynamic stream settings.""" + return DynamicStreamSettings() diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index e9da793369fd40..cd4f579693834d 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -16,7 +16,7 @@ MAX_SEGMENTS, NUM_PLAYLIST_SEGMENTS, ) -from homeassistant.components.stream.core import Part +from homeassistant.components.stream.core import Orientation, Part from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -24,6 +24,7 @@ FAKE_TIME, DefaultSegment as Segment, assert_mp4_has_transform_matrix, + dynamic_stream_settings, ) from tests.common import async_fire_time_changed @@ -145,7 +146,7 @@ async def test_hls_stream( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -185,7 +186,7 @@ async def test_hls_stream( assert stream.get_diagnostics() == { "container_format": "mov,mp4,m4a,3gp,3g2,mj2", "keepalive": False, - "orientation": 1, + "orientation": Orientation.NO_TRANSFORM, "start_worker": 1, "video_codec": "h264", "worker_error": 1, @@ -199,7 +200,7 @@ async def test_stream_timeout( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) available_states = [] @@ -252,7 +253,7 @@ async def test_stream_timeout_after_stop( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -272,7 +273,7 @@ async def test_stream_retries(hass, setup_component, should_retry): """Test hls stream is retried on failure.""" # Setup demo HLS track source = "test_stream_keepalive_source" - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, dynamic_stream_settings()) track = stream.add_provider(HLS_PROVIDER) track.num_segments = 2 @@ -320,7 +321,7 @@ def time_side_effect(): async def test_hls_playlist_view_no_output(hass, setup_component, hls_stream): """Test rendering the hls playlist with no output segments.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream.add_provider(HLS_PROVIDER) hls_client = await hls_stream(stream) @@ -332,7 +333,7 @@ async def test_hls_playlist_view_no_output(hass, setup_component, hls_stream): async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worker_sync): """Test rendering the hls playlist with 1 and 2 output segments.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) for i in range(2): @@ -363,7 +364,7 @@ async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worke async def test_hls_max_segments(hass, setup_component, hls_stream, stream_worker_sync): """Test rendering the hls playlist with more segments than the segment deque can hold.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -415,7 +416,7 @@ async def test_hls_playlist_view_discontinuity( ): """Test a discontinuity across segments in the stream with 3 segments.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -452,7 +453,7 @@ async def test_hls_max_segments_discontinuity( hass, setup_component, hls_stream, stream_worker_sync ): """Test a discontinuity with more segments than the segment deque can hold.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -495,7 +496,7 @@ async def test_remove_incomplete_segment_on_exit( hass, setup_component, stream_worker_sync ): """Test that the incomplete segment gets removed when the worker thread quits.""" - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() await stream.start() hls = stream.add_provider(HLS_PROVIDER) @@ -536,7 +537,7 @@ async def test_hls_stream_rotate( stream_worker_sync.pause() # Setup demo HLS track - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -549,14 +550,14 @@ async def test_hls_stream_rotate( assert master_playlist_response.status == HTTPStatus.OK # Fetch rotated init - stream.orientation = 6 + stream.dynamic_stream_settings.orientation = Orientation.ROTATE_LEFT init_response = await hls_client.get("/init.mp4") assert init_response.status == HTTPStatus.OK init = await init_response.read() stream_worker_sync.resume() - assert_mp4_has_transform_matrix(init, stream.orientation) + assert_mp4_has_transform_matrix(init, stream.dynamic_stream_settings.orientation) # Stop stream, if it hasn't quit already await stream.stop() diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index baad304354749b..448c3593d68c48 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -22,7 +22,12 @@ from homeassistant.components.stream.core import Part from homeassistant.setup import async_setup_component -from .common import FAKE_TIME, DefaultSegment as Segment, generate_h264_video +from .common import ( + FAKE_TIME, + DefaultSegment as Segment, + dynamic_stream_settings, + generate_h264_video, +) from .test_hls import STREAM_SOURCE, HlsClient, make_playlist SEGMENT_DURATION = 6 @@ -135,7 +140,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): num_playlist_segments = 3 # Setup demo HLS track source = generate_h264_video(duration=num_playlist_segments * SEGMENT_DURATION + 2) - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, dynamic_stream_settings()) # Request stream stream.add_provider(HLS_PROVIDER) @@ -259,7 +264,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -328,7 +333,7 @@ async def test_ll_hls_msn(hass, hls_stream, stream_worker_sync, hls_sync): }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -393,7 +398,7 @@ async def test_ll_hls_playlist_bad_msn_part(hass, hls_stream, stream_worker_sync }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -462,7 +467,7 @@ async def test_ll_hls_playlist_rollover_part( }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -541,7 +546,7 @@ async def test_ll_hls_playlist_msn_part(hass, hls_stream, stream_worker_sync, hl }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) @@ -607,7 +612,7 @@ async def test_get_part_segments(hass, hls_stream, stream_worker_sync, hls_sync) }, ) - stream = create_stream(hass, STREAM_SOURCE, {}) + stream = create_stream(hass, STREAM_SOURCE, {}, dynamic_stream_settings()) stream_worker_sync.pause() hls = stream.add_provider(HLS_PROVIDER) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index c07675c77127ac..1e941dd5d814ad 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -14,7 +14,7 @@ OUTPUT_IDLE_TIMEOUT, RECORDER_PROVIDER, ) -from homeassistant.components.stream.core import Part +from homeassistant.components.stream.core import Orientation, Part from homeassistant.components.stream.fmp4utils import find_box from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -23,6 +23,7 @@ from .common import ( DefaultSegment as Segment, assert_mp4_has_transform_matrix, + dynamic_stream_settings, generate_h264_video, remux_with_audio, ) @@ -56,7 +57,7 @@ async def remove_provider(self, provider): worker_finished.set() with patch("homeassistant.components.stream.Stream", wraps=MockStream): - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -79,7 +80,7 @@ async def remove_provider(self, provider): async def test_record_lookback(hass, filename, h264_video): """Exercise record with lookback.""" - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) # Start an HLS feed to enable lookback stream.add_provider(HLS_PROVIDER) @@ -96,7 +97,7 @@ async def test_record_lookback(hass, filename, h264_video): async def test_record_path_not_allowed(hass, h264_video): """Test where the output path is not allowed by home assistant configuration.""" - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) with patch.object( hass.config, "is_allowed_path", return_value=False ), pytest.raises(HomeAssistantError): @@ -146,7 +147,7 @@ def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT): with patch.object(hass.config, "is_allowed_path", return_value=True), patch( "homeassistant.components.stream.Stream", wraps=MockStream ), patch("homeassistant.components.stream.recorder.RecorderOutput.recv"): - stream = create_stream(hass, "blank", {}) + stream = create_stream(hass, "blank", {}, dynamic_stream_settings()) make_recording = hass.async_create_task(stream.async_record(filename)) await provider_ready.wait() @@ -166,7 +167,7 @@ def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT): async def test_recorder_no_segments(hass, filename): """Test recorder behavior with a stream failure which causes no segments.""" - stream = create_stream(hass, BytesIO(), {}) + stream = create_stream(hass, BytesIO(), {}, dynamic_stream_settings()) # Run with patch.object(hass.config, "is_allowed_path", return_value=True): @@ -219,7 +220,7 @@ async def remove_provider(self, provider): worker_finished.set() with patch("homeassistant.components.stream.Stream", wraps=MockStream): - stream = create_stream(hass, source, {}) + stream = create_stream(hass, source, {}, dynamic_stream_settings()) with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -252,7 +253,9 @@ async def remove_provider(self, provider): async def test_recorder_log(hass, filename, caplog): """Test starting a stream to record logs the url without username and password.""" - stream = create_stream(hass, "https://abcd:efgh@foo.bar", {}) + stream = create_stream( + hass, "https://abcd:efgh@foo.bar", {}, dynamic_stream_settings() + ) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record(filename) assert "https://abcd:efgh@foo.bar" not in caplog.text @@ -273,8 +276,8 @@ async def remove_provider(self, provider): worker_finished.set() with patch("homeassistant.components.stream.Stream", wraps=MockStream): - stream = create_stream(hass, h264_video, {}) - stream.orientation = 8 + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) + stream.dynamic_stream_settings.orientation = Orientation.ROTATE_RIGHT with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -293,4 +296,6 @@ async def remove_provider(self, provider): # Assert assert os.path.exists(filename) with open(filename, "rb") as rotated_mp4: - assert_mp4_has_transform_matrix(rotated_mp4.read(), stream.orientation) + assert_mp4_has_transform_matrix( + rotated_mp4.read(), stream.dynamic_stream_settings.orientation + ) diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index e77b062fa9c820..e9f5769ddf11f6 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -39,7 +39,7 @@ SEGMENT_DURATION_ADJUSTER, TARGET_SEGMENT_DURATION_NON_LL_HLS, ) -from homeassistant.components.stream.core import StreamSettings +from homeassistant.components.stream.core import Orientation, StreamSettings from homeassistant.components.stream.worker import ( StreamEndedError, StreamState, @@ -48,7 +48,7 @@ ) from homeassistant.setup import async_setup_component -from .common import generate_h264_video, generate_h265_video +from .common import dynamic_stream_settings, generate_h264_video, generate_h265_video from .test_ll_hls import TEST_PART_DURATION from tests.components.camera.common import EMPTY_8_6_JPEG, mock_turbo_jpeg @@ -90,7 +90,6 @@ def mock_stream_settings(hass): part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, hls_advance_part_limit=3, hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - orientation=1, ) } @@ -287,7 +286,7 @@ def run_worker(hass, stream, stream_source, stream_settings=None): {}, stream_settings or hass.data[DOMAIN][ATTR_SETTINGS], stream_state, - KeyFrameConverter(hass, 1), + KeyFrameConverter(hass, stream_settings, dynamic_stream_settings()), threading.Event(), ) @@ -295,7 +294,11 @@ def run_worker(hass, stream, stream_source, stream_settings=None): async def async_decode_stream(hass, packets, py_av=None, stream_settings=None): """Start a stream worker that decodes incoming stream packets into output segments.""" stream = Stream( - hass, STREAM_SOURCE, {}, stream_settings or hass.data[DOMAIN][ATTR_SETTINGS] + hass, + STREAM_SOURCE, + {}, + stream_settings or hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), ) stream.add_provider(HLS_PROVIDER) @@ -322,7 +325,13 @@ async def async_decode_stream(hass, packets, py_av=None, stream_settings=None): async def test_stream_open_fails(hass): """Test failure on stream open.""" - stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) + stream = Stream( + hass, + STREAM_SOURCE, + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), + ) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open, pytest.raises(StreamWorkerError): av_open.side_effect = av.error.InvalidDataError(-2, "error") @@ -636,7 +645,13 @@ async def test_stream_stopped_while_decoding(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) + stream = Stream( + hass, + STREAM_SOURCE, + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), + ) stream.add_provider(HLS_PROVIDER) py_av = MockPyAv() @@ -666,7 +681,13 @@ async def test_update_stream_source(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) + stream = Stream( + hass, + STREAM_SOURCE, + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), + ) stream.add_provider(HLS_PROVIDER) # Note that retries are disabled by default in tests, however the stream is "restarted" when # the stream source is updated. @@ -709,7 +730,11 @@ def blocking_open(stream_source, *args, **kwargs): async def test_worker_log(hass, caplog): """Test that the worker logs the url without username and password.""" stream = Stream( - hass, "https://abcd:efgh@foo.bar", {}, hass.data[DOMAIN][ATTR_SETTINGS] + hass, + "https://abcd:efgh@foo.bar", + {}, + hass.data[DOMAIN][ATTR_SETTINGS], + dynamic_stream_settings(), ) stream.add_provider(HLS_PROVIDER) @@ -764,7 +789,9 @@ async def test_durations(hass, worker_finished_stream): worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): - stream = create_stream(hass, source, {}, stream_label="camera") + stream = create_stream( + hass, source, {}, dynamic_stream_settings(), stream_label="camera" + ) recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) await stream.start() @@ -839,7 +866,9 @@ async def test_has_keyframe(hass, h264_video, worker_finished_stream): worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): - stream = create_stream(hass, h264_video, {}, stream_label="camera") + stream = create_stream( + hass, h264_video, {}, dynamic_stream_settings(), stream_label="camera" + ) recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) await stream.start() @@ -880,7 +909,9 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream): worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): - stream = create_stream(hass, source, {}, stream_label="camera") + stream = create_stream( + hass, source, {}, dynamic_stream_settings(), stream_label="camera" + ) recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) await stream.start() @@ -900,7 +931,7 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream): assert stream.get_diagnostics() == { "container_format": "mov,mp4,m4a,3gp,3g2,mj2", "keepalive": False, - "orientation": 1, + "orientation": Orientation.NO_TRANSFORM, "start_worker": 1, "video_codec": "hevc", "worker_error": 1, @@ -916,7 +947,7 @@ async def test_get_image(hass, h264_video, filename): "homeassistant.components.camera.img_util.TurboJPEGSingleton" ) as mock_turbo_jpeg_singleton: mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() - stream = create_stream(hass, h264_video, {}) + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) @@ -937,7 +968,6 @@ async def test_worker_disable_ll_hls(hass): part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, hls_advance_part_limit=3, hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - orientation=1, ) py_av = MockPyAv() py_av.container.format.name = "hls" @@ -959,9 +989,9 @@ async def test_get_image_rotated(hass, h264_video, filename): "homeassistant.components.camera.img_util.TurboJPEGSingleton" ) as mock_turbo_jpeg_singleton: mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() - for orientation in (1, 8): - stream = create_stream(hass, h264_video, {}) - stream._stream_settings.orientation = orientation + for orientation in (Orientation.NO_TRANSFORM, Orientation.ROTATE_RIGHT): + stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) + stream.dynamic_stream_settings.orientation = orientation with patch.object(hass.config, "is_allowed_path", return_value=True): make_recording = hass.async_create_task(stream.async_record(filename)) From 4bb1f4ec7909c25704a715c8798b3a1b05185ee6 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 12 Nov 2022 22:29:31 +0200 Subject: [PATCH 389/394] Add Armed binary sensor to local Risco (#81997) --- .../components/risco/binary_sensor.py | 25 +++++- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/risco/conftest.py | 4 + tests/components/risco/test_binary_sensor.py | 86 +++++++++++-------- 6 files changed, 82 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/risco/binary_sensor.py b/homeassistant/components/risco/binary_sensor.py index b1f55dd8693c94..7c9733e2ab962f 100644 --- a/homeassistant/components/risco/binary_sensor.py +++ b/homeassistant/components/risco/binary_sensor.py @@ -35,6 +35,10 @@ async def async_setup_entry( RiscoLocalAlarmedBinarySensor(local_data.system.id, zone_id, zone) for zone_id, zone in local_data.system.zones.items() ) + async_add_entities( + RiscoLocalArmedBinarySensor(local_data.system.id, zone_id, zone) + for zone_id, zone in local_data.system.zones.items() + ) else: coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id @@ -92,8 +96,6 @@ def is_on(self) -> bool | None: class RiscoLocalAlarmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): """Representation whether a zone in Risco local is currently triggering an alarm.""" - _attr_should_poll = False - def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: """Init the zone.""" super().__init__( @@ -108,3 +110,22 @@ def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._zone.alarmed + + +class RiscoLocalArmedBinarySensor(RiscoLocalZoneEntity, BinarySensorEntity): + """Representation whether a zone in Risco local is currently armed.""" + + def __init__(self, system_id: str, zone_id: int, zone: Zone) -> None: + """Init the zone.""" + super().__init__( + system_id=system_id, + name="Armed", + suffix="_armed", + zone_id=zone_id, + zone=zone, + ) + + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return self._zone.armed diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index bd8bbfd715ff22..d31d148f4dac9b 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.5"], + "requirements": ["pyrisco==0.5.6"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ead4f05bf9d042..6290759fe210a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1860,7 +1860,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.5 +pyrisco==0.5.6 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8661001022ac8..d07aeff4b7bf50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1316,7 +1316,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.5 +pyrisco==0.5.6 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index cc65efd9b555a3..9d14cfdcff0b1f 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -70,6 +70,8 @@ def two_zone_local(): zone_mocks[0], "alarmed", new_callable=PropertyMock(return_value=False) ), patch.object( zone_mocks[0], "bypassed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[0], "armed", new_callable=PropertyMock(return_value=False) ), patch.object( zone_mocks[1], "id", new_callable=PropertyMock(return_value=1) ), patch.object( @@ -78,6 +80,8 @@ def two_zone_local(): zone_mocks[1], "alarmed", new_callable=PropertyMock(return_value=False) ), patch.object( zone_mocks[1], "bypassed", new_callable=PropertyMock(return_value=False) + ), patch.object( + zone_mocks[1], "armed", new_callable=PropertyMock(return_value=False) ), patch( "homeassistant.components.risco.RiscoLocal.partitions", new_callable=PropertyMock(return_value={}), diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 00d10f6059e43d..1c331adc145db5 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -15,6 +15,8 @@ SECOND_ENTITY_ID = "binary_sensor.zone_1" FIRST_ALARMED_ENTITY_ID = FIRST_ENTITY_ID + "_alarmed" SECOND_ALARMED_ENTITY_ID = SECOND_ENTITY_ID + "_alarmed" +FIRST_ARMED_ENTITY_ID = FIRST_ENTITY_ID + "_armed" +SECOND_ARMED_ENTITY_ID = SECOND_ENTITY_ID + "_armed" @pytest.mark.parametrize("exception", [CannotConnectError, UnauthorizedError]) @@ -95,33 +97,19 @@ async def test_local_setup(hass, two_zone_local, setup_risco_local): assert device.manufacturer == "Risco" -async def _check_local_state(hass, zones, triggered, entity_id, zone_id, callback): - with patch.object( - zones[zone_id], - "triggered", - new_callable=PropertyMock(return_value=triggered), - ): - await callback(zone_id, zones[zone_id]) - await hass.async_block_till_done() - - expected_triggered = STATE_ON if triggered else STATE_OFF - assert hass.states.get(entity_id).state == expected_triggered - assert hass.states.get(entity_id).attributes["zone_id"] == zone_id - - -async def _check_alarmed_local_state( - hass, zones, alarmed, entity_id, zone_id, callback +async def _check_local_state( + hass, zones, property, value, entity_id, zone_id, callback ): with patch.object( zones[zone_id], - "alarmed", - new_callable=PropertyMock(return_value=alarmed), + property, + new_callable=PropertyMock(return_value=value), ): await callback(zone_id, zones[zone_id]) await hass.async_block_till_done() - expected_alarmed = STATE_ON if alarmed else STATE_OFF - assert hass.states.get(entity_id).state == expected_alarmed + expected_value = STATE_ON if value else STATE_OFF + assert hass.states.get(entity_id).state == expected_value assert hass.states.get(entity_id).attributes["zone_id"] == zone_id @@ -134,34 +122,64 @@ def _mock_zone_handler(): async def test_local_states( hass, two_zone_local, _mock_zone_handler, setup_risco_local ): - """Test the various alarm states.""" + """Test the various zone states.""" callback = _mock_zone_handler.call_args.args[0] assert callback is not None - await _check_local_state(hass, two_zone_local, True, FIRST_ENTITY_ID, 0, callback) - await _check_local_state(hass, two_zone_local, False, FIRST_ENTITY_ID, 0, callback) - await _check_local_state(hass, two_zone_local, True, SECOND_ENTITY_ID, 1, callback) - await _check_local_state(hass, two_zone_local, False, SECOND_ENTITY_ID, 1, callback) + await _check_local_state( + hass, two_zone_local, "triggered", True, FIRST_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, "triggered", False, FIRST_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, "triggered", True, SECOND_ENTITY_ID, 1, callback + ) + await _check_local_state( + hass, two_zone_local, "triggered", False, SECOND_ENTITY_ID, 1, callback + ) async def test_alarmed_local_states( hass, two_zone_local, _mock_zone_handler, setup_risco_local ): - """Test the various alarm states.""" + """Test the various zone alarmed states.""" + callback = _mock_zone_handler.call_args.args[0] + + assert callback is not None + + await _check_local_state( + hass, two_zone_local, "alarmed", True, FIRST_ALARMED_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, "alarmed", False, FIRST_ALARMED_ENTITY_ID, 0, callback + ) + await _check_local_state( + hass, two_zone_local, "alarmed", True, SECOND_ALARMED_ENTITY_ID, 1, callback + ) + await _check_local_state( + hass, two_zone_local, "alarmed", False, SECOND_ALARMED_ENTITY_ID, 1, callback + ) + + +async def test_armed_local_states( + hass, two_zone_local, _mock_zone_handler, setup_risco_local +): + """Test the various zone armed states.""" callback = _mock_zone_handler.call_args.args[0] assert callback is not None - await _check_alarmed_local_state( - hass, two_zone_local, True, FIRST_ALARMED_ENTITY_ID, 0, callback + await _check_local_state( + hass, two_zone_local, "armed", True, FIRST_ARMED_ENTITY_ID, 0, callback ) - await _check_alarmed_local_state( - hass, two_zone_local, False, FIRST_ALARMED_ENTITY_ID, 0, callback + await _check_local_state( + hass, two_zone_local, "armed", False, FIRST_ARMED_ENTITY_ID, 0, callback ) - await _check_alarmed_local_state( - hass, two_zone_local, True, SECOND_ALARMED_ENTITY_ID, 1, callback + await _check_local_state( + hass, two_zone_local, "armed", True, SECOND_ARMED_ENTITY_ID, 1, callback ) - await _check_alarmed_local_state( - hass, two_zone_local, False, SECOND_ALARMED_ENTITY_ID, 1, callback + await _check_local_state( + hass, two_zone_local, "armed", False, SECOND_ARMED_ENTITY_ID, 1, callback ) From b6c27585c74294fd4cc4d3a2640cf98ef6b4c343 Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Sat, 12 Nov 2022 23:59:29 +0200 Subject: [PATCH 390/394] Implemented RestoreEntity for Dynalite (#73911) * Implemented RestoreEntity Merged commit conflict * removed accidental change * Update homeassistant/components/dynalite/dynalitebase.py Co-authored-by: Erik Montnemery * added tests for the state * added tests for switch state * moved to ATTR_x and STATE_x instead of strings some fixes to test_cover * moved blind to DEVICE_CLASS_BLIND * used correct constant instead of deprecated * Implemented RestoreEntity * removed accidental change * added tests for the state * added tests for switch state * moved to ATTR_x and STATE_x instead of strings some fixes to test_cover * fixed isort issue from merge Co-authored-by: Erik Montnemery --- homeassistant/components/dynalite/cover.py | 13 +++- .../components/dynalite/dynalitebase.py | 20 ++++- homeassistant/components/dynalite/light.py | 8 +- .../components/dynalite/manifest.json | 2 +- homeassistant/components/dynalite/switch.py | 6 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dynalite/test_bridge.py | 4 + tests/components/dynalite/test_cover.py | 74 ++++++++++++++++--- tests/components/dynalite/test_light.py | 51 ++++++++++++- tests/components/dynalite/test_switch.py | 35 ++++++++- 11 files changed, 194 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index e5c38996a89865..bbca16d3db6da8 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -2,7 +2,12 @@ from typing import Any -from homeassistant.components.cover import DEVICE_CLASSES, CoverDeviceClass, CoverEntity +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + DEVICE_CLASSES, + CoverDeviceClass, + CoverEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,6 +83,12 @@ async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._device.async_stop_cover(**kwargs) + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = state.attributes.get(ATTR_CURRENT_POSITION) + if target_level is not None: + self._device.init_level(target_level) + class DynaliteCoverWithTilt(DynaliteCover): """Representation of a Dynalite Channel as a Home Assistant Cover that uses up and down for tilt.""" diff --git a/homeassistant/components/dynalite/dynalitebase.py b/homeassistant/components/dynalite/dynalitebase.py index b4b8285cbb011a..3ebf04ab219adb 100644 --- a/homeassistant/components/dynalite/dynalitebase.py +++ b/homeassistant/components/dynalite/dynalitebase.py @@ -1,14 +1,16 @@ """Support for the Dynalite devices as entities.""" from __future__ import annotations +from abc import ABC, abstractmethod from collections.abc import Callable from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from .bridge import DynaliteBridge from .const import DOMAIN, LOGGER @@ -36,7 +38,7 @@ def async_add_entities_platform(devices): bridge.register_add_devices(platform, async_add_entities_platform) -class DynaliteBase(Entity): +class DynaliteBase(RestoreEntity, ABC): """Base class for the Dynalite entities.""" def __init__(self, device: Any, bridge: DynaliteBridge) -> None: @@ -70,8 +72,16 @@ def device_info(self) -> DeviceInfo: ) async def async_added_to_hass(self) -> None: - """Added to hass so need to register to dispatch.""" + """Added to hass so need to restore state and register to dispatch.""" # register for device specific update + await super().async_added_to_hass() + + cur_state = await self.async_get_last_state() + if cur_state: + self.initialize_state(cur_state) + else: + LOGGER.info("Restore state not available for %s", self.entity_id) + self._unsub_dispatchers.append( async_dispatcher_connect( self.hass, @@ -88,6 +98,10 @@ async def async_added_to_hass(self) -> None: ) ) + @abstractmethod + def initialize_state(self, state): + """Initialize the state from cache.""" + async def async_will_remove_from_hass(self) -> None: """Unregister signal dispatch listeners when being removed.""" for unsub in self._unsub_dispatchers: diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index 27cd6f8cae8a6d..ffb97da49c1c56 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -2,7 +2,7 @@ from typing import Any -from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -44,3 +44,9 @@ async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.async_turn_off(**kwargs) + + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = state.attributes.get(ATTR_BRIGHTNESS) + if target_level is not None: + self._device.init_level(target_level) diff --git a/homeassistant/components/dynalite/manifest.json b/homeassistant/components/dynalite/manifest.json index d403291a081449..57010666019484 100644 --- a/homeassistant/components/dynalite/manifest.json +++ b/homeassistant/components/dynalite/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dynalite", "codeowners": ["@ziv1234"], - "requirements": ["dynalite_devices==0.1.46"], + "requirements": ["dynalite_devices==0.1.47"], "iot_class": "local_push", "loggers": ["dynalite_devices_lib"] } diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index 3e459e45847bf4..54e9b919b89c5d 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -4,6 +4,7 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -36,3 +37,8 @@ async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.async_turn_off() + + def initialize_state(self, state): + """Initialize the state from cache.""" + target_level = 1 if state.state == STATE_ON else 0 + self._device.init_level(target_level) diff --git a/requirements_all.txt b/requirements_all.txt index 6290759fe210a9..5f1a115044238c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -609,7 +609,7 @@ dwdwfsapi==1.0.5 dweepy==0.3.0 # homeassistant.components.dynalite -dynalite_devices==0.1.46 +dynalite_devices==0.1.47 # homeassistant.components.rainforest_eagle eagle100==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d07aeff4b7bf50..d43ecede70cbb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -471,7 +471,7 @@ doorbirdpy==2.1.0 dsmr_parser==0.33 # homeassistant.components.dynalite -dynalite_devices==0.1.46 +dynalite_devices==0.1.47 # homeassistant.components.rainforest_eagle eagle100==0.1.1 diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 363a9671f59bf0..f5cfaec7a97d8d 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -70,10 +70,12 @@ async def test_add_devices_then_register(hass): device1.category = "light" device1.name = "NAME" device1.unique_id = "unique1" + device1.brightness = 1 device2 = Mock() device2.category = "switch" device2.name = "NAME2" device2.unique_id = "unique2" + device2.brightness = 1 new_device_func([device1, device2]) device3 = Mock() device3.category = "switch" @@ -103,10 +105,12 @@ async def test_register_then_add_devices(hass): device1.category = "light" device1.name = "NAME" device1.unique_id = "unique1" + device1.brightness = 1 device2 = Mock() device2.category = "switch" device2.name = "NAME2" device2.unique_id = "unique2" + device2.brightness = 1 new_device_func([device1, device2]) await hass.async_block_till_done() assert hass.states.get("light.name") diff --git a/tests/components/dynalite/test_cover.py b/tests/components/dynalite/test_cover.py index fd671365ba1170..5fbb22b91a78f6 100644 --- a/tests/components/dynalite/test_cover.py +++ b/tests/components/dynalite/test_cover.py @@ -1,8 +1,25 @@ """Test Dynalite cover.""" +from unittest.mock import Mock + from dynalite_devices_lib.cover import DynaliteTimeCoverWithTiltDevice import pytest -from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, + CoverDeviceClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import State from homeassistant.exceptions import HomeAssistantError from .common import ( @@ -14,12 +31,25 @@ run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" mock_dev = create_mock_device("cover", DynaliteTimeCoverWithTiltDevice) - mock_dev.device_class = "blind" + mock_dev.device_class = CoverDeviceClass.BLIND.value + mock_dev.current_cover_position = 0 + mock_dev.current_cover_tilt_position = 0 + mock_dev.is_opening = False + mock_dev.is_closing = False + mock_dev.is_closed = True + + def mock_init_level(target): + mock_dev.is_closed = target == 0 + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev @@ -29,11 +59,11 @@ async def test_cover_setup(hass, mock_device): entity_state = hass.states.get("cover.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name assert ( - entity_state.attributes["current_position"] + entity_state.attributes[ATTR_CURRENT_POSITION] == mock_device.current_cover_position ) assert ( - entity_state.attributes["current_tilt_position"] + entity_state.attributes[ATTR_CURRENT_TILT_POSITION] == mock_device.current_cover_tilt_position ) assert entity_state.attributes[ATTR_DEVICE_CLASS] == mock_device.device_class @@ -48,7 +78,7 @@ async def test_cover_setup(hass, mock_device): { ATTR_SERVICE: "set_cover_position", ATTR_METHOD: "async_set_cover_position", - ATTR_ARGS: {"position": 50}, + ATTR_ARGS: {ATTR_POSITION: 50}, }, {ATTR_SERVICE: "open_cover_tilt", ATTR_METHOD: "async_open_cover_tilt"}, {ATTR_SERVICE: "close_cover_tilt", ATTR_METHOD: "async_close_cover_tilt"}, @@ -56,7 +86,7 @@ async def test_cover_setup(hass, mock_device): { ATTR_SERVICE: "set_cover_tilt_position", ATTR_METHOD: "async_set_cover_tilt_position", - ATTR_ARGS: {"tilt_position": 50}, + ATTR_ARGS: {ATTR_TILT_POSITION: 50}, }, ], ) @@ -91,14 +121,38 @@ async def test_cover_positions(hass, mock_device): """Test that the state updates in the various positions.""" update_func = await create_entity_from_device(hass, mock_device) await check_cover_position( - hass, update_func, mock_device, True, False, False, "closing" + hass, update_func, mock_device, True, False, False, STATE_CLOSING ) await check_cover_position( - hass, update_func, mock_device, False, True, False, "opening" + hass, update_func, mock_device, False, True, False, STATE_OPENING ) await check_cover_position( - hass, update_func, mock_device, False, False, True, "closed" + hass, update_func, mock_device, False, False, True, STATE_CLOSED ) await check_cover_position( - hass, update_func, mock_device, False, False, False, "open" + hass, update_func, mock_device, False, False, False, STATE_OPEN ) + + +async def test_cover_restore_state(hass, mock_device): + """Test restore from cache.""" + mock_restore_cache( + hass, + [State("cover.name", STATE_OPEN, attributes={ATTR_CURRENT_POSITION: 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(77) + entity_state = hass.states.get("cover.name") + assert entity_state.state == STATE_OPEN + + +async def test_cover_restore_state_bad_cache(hass, mock_device): + """Test restore from a cache without the attribute.""" + mock_restore_cache( + hass, + [State("cover.name", STATE_OPEN, attributes={"bla bla": 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_not_called() + entity_state = hass.states.get("cover.name") + assert entity_state.state == STATE_CLOSED diff --git a/tests/components/dynalite/test_light.py b/tests/components/dynalite/test_light.py index b100cf8d3f6a73..337f0a415e6b79 100644 --- a/tests/components/dynalite/test_light.py +++ b/tests/components/dynalite/test_light.py @@ -1,8 +1,11 @@ """Test Dynalite light.""" +from unittest.mock import Mock, PropertyMock + from dynalite_devices_lib.light import DynaliteChannelLightDevice import pytest from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_SUPPORTED_COLOR_MODES, ColorMode, @@ -10,8 +13,11 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import State from .common import ( ATTR_METHOD, @@ -22,11 +28,25 @@ run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" - return create_mock_device("light", DynaliteChannelLightDevice) + mock_dev = create_mock_device("light", DynaliteChannelLightDevice) + mock_dev.brightness = 0 + + def mock_is_on(): + return mock_dev.brightness != 0 + + type(mock_dev).is_on = PropertyMock(side_effect=mock_is_on) + + def mock_init_level(target): + mock_dev.brightness = target + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev async def test_light_setup(hass, mock_device): @@ -34,10 +54,9 @@ async def test_light_setup(hass, mock_device): await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("light.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name - assert entity_state.attributes["brightness"] == mock_device.brightness - assert entity_state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS assert entity_state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] assert entity_state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert entity_state.state == STATE_OFF await run_service_tests( hass, mock_device, @@ -67,3 +86,29 @@ async def test_remove_config_entry(hass, mock_device): assert await hass.config_entries.async_remove(entry_id) await hass.async_block_till_done() assert not hass.states.get("light.name") + + +async def test_light_restore_state(hass, mock_device): + """Test restore from cache.""" + mock_restore_cache( + hass, + [State("light.name", STATE_ON, attributes={ATTR_BRIGHTNESS: 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(77) + entity_state = hass.states.get("light.name") + assert entity_state.state == STATE_ON + assert entity_state.attributes[ATTR_BRIGHTNESS] == 77 + assert entity_state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + + +async def test_light_restore_state_bad_cache(hass, mock_device): + """Test restore from a cache without the attribute.""" + mock_restore_cache( + hass, + [State("light.name", "abc", attributes={"blabla": 77})], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_not_called() + entity_state = hass.states.get("light.name") + assert entity_state.state == STATE_OFF diff --git a/tests/components/dynalite/test_switch.py b/tests/components/dynalite/test_switch.py index de375e3b348d7f..95ab64ef197213 100644 --- a/tests/components/dynalite/test_switch.py +++ b/tests/components/dynalite/test_switch.py @@ -1,9 +1,12 @@ """Test Dynalite switch.""" +from unittest.mock import Mock + from dynalite_devices_lib.switch import DynalitePresetSwitchDevice import pytest -from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF, STATE_ON +from homeassistant.core import State from .common import ( ATTR_METHOD, @@ -13,11 +16,20 @@ run_service_tests, ) +from tests.common import mock_restore_cache + @pytest.fixture def mock_device(): """Mock a Dynalite device.""" - return create_mock_device("switch", DynalitePresetSwitchDevice) + mock_dev = create_mock_device("switch", DynalitePresetSwitchDevice) + mock_dev.is_on = False + + def mock_init_level(level): + mock_dev.is_on = level + + type(mock_dev).init_level = Mock(side_effect=mock_init_level) + return mock_dev async def test_switch_setup(hass, mock_device): @@ -25,6 +37,7 @@ async def test_switch_setup(hass, mock_device): await create_entity_from_device(hass, mock_device) entity_state = hass.states.get("switch.name") assert entity_state.attributes[ATTR_FRIENDLY_NAME] == mock_device.name + assert entity_state.state == STATE_OFF await run_service_tests( hass, mock_device, @@ -34,3 +47,21 @@ async def test_switch_setup(hass, mock_device): {ATTR_SERVICE: "turn_off", ATTR_METHOD: "async_turn_off"}, ], ) + + +@pytest.mark.parametrize("saved_state, level", [(STATE_ON, 1), (STATE_OFF, 0)]) +async def test_switch_restore_state(hass, mock_device, saved_state, level): + """Test restore from cache.""" + mock_restore_cache( + hass, + [ + State( + "switch.name", + saved_state, + ) + ], + ) + await create_entity_from_device(hass, mock_device) + mock_device.init_level.assert_called_once_with(level) + entity_state = hass.states.get("switch.name") + assert entity_state.state == saved_state From 5e610cdfd2bac229cbc2a8253c7f102e792c5ff0 Mon Sep 17 00:00:00 2001 From: Cougar Date: Sun, 13 Nov 2022 00:24:33 +0200 Subject: [PATCH 391/394] Bump luftdaten to 0.7.4 (#82002) Bump luftdaten to 0.7.4 (#61687) changelog: https://github.com/home-assistant-ecosystem/python-luftdaten/compare/0.7.2...0.7.4 Fixes API connection error handling (#61687) --- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index aed8d80f8b1c0e..4c84c81af5ef7a 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,7 +3,7 @@ "name": "Sensor.Community", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": ["luftdaten==0.7.2"], + "requirements": ["luftdaten==0.7.4"], "codeowners": ["@fabaff", "@frenck"], "quality_scale": "gold", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 5f1a115044238c..20ea4c7862efb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1043,7 +1043,7 @@ logi_circle==0.2.3 london-tube-status==0.5 # homeassistant.components.luftdaten -luftdaten==0.7.2 +luftdaten==0.7.4 # homeassistant.components.lupusec lupupy==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d43ecede70cbb0..f365317a7adfc1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -763,7 +763,7 @@ life360==5.3.0 logi_circle==0.2.3 # homeassistant.components.luftdaten -luftdaten==0.7.2 +luftdaten==0.7.4 # homeassistant.components.scrape lxml==4.9.1 From fbcba840adc746b8e946be90f934fac5dd634da6 Mon Sep 17 00:00:00 2001 From: "mark.r.godwin@gmail.com" Date: Sat, 29 Oct 2022 17:12:19 +0000 Subject: [PATCH 392/394] TP-Link Omada integration Support for PoE config of network switch ports --- .coveragerc | 4 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/tplink_omada/__init__.py | 40 ++++++ .../components/tplink_omada/config_flow.py | 118 ++++++++++++++++ .../components/tplink_omada/const.py | 3 + .../components/tplink_omada/coordinator.py | 44 ++++++ .../components/tplink_omada/entity.py | 33 +++++ .../components/tplink_omada/manifest.json | 13 ++ .../components/tplink_omada/strings.json | 24 ++++ .../components/tplink_omada/switch.py | 126 ++++++++++++++++++ .../tplink_omada/translations/en.json | 23 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + mypy.ini | 10 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/tplink_omada/__init__.py | 1 + .../tplink_omada/test_config_flow.py | 98 ++++++++++++++ 19 files changed, 553 insertions(+) create mode 100644 homeassistant/components/tplink_omada/__init__.py create mode 100644 homeassistant/components/tplink_omada/config_flow.py create mode 100644 homeassistant/components/tplink_omada/const.py create mode 100644 homeassistant/components/tplink_omada/coordinator.py create mode 100644 homeassistant/components/tplink_omada/entity.py create mode 100644 homeassistant/components/tplink_omada/manifest.json create mode 100644 homeassistant/components/tplink_omada/strings.json create mode 100644 homeassistant/components/tplink_omada/switch.py create mode 100644 homeassistant/components/tplink_omada/translations/en.json create mode 100644 tests/components/tplink_omada/__init__.py create mode 100644 tests/components/tplink_omada/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 1a8bfb462d8d84..8f9060043ad8c8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1354,6 +1354,10 @@ omit = homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py homeassistant/components/tplink_lte/* + homeassistant/components/tplink_omada/__init__.py + homeassistant/components/tplink_omada/coordinator.py + homeassistant/components/tplink_omada/entity.py + homeassistant/components/tplink_omada/switch.py homeassistant/components/traccar/const.py homeassistant/components/traccar/device_tracker.py homeassistant/components/tractive/__init__.py diff --git a/.strict-typing b/.strict-typing index d45fe269638326..848c917e874764 100644 --- a/.strict-typing +++ b/.strict-typing @@ -268,6 +268,7 @@ homeassistant.components.tile.* homeassistant.components.tilt_ble.* homeassistant.components.tolo.* homeassistant.components.tplink.* +homeassistant.components.tplink_omada.* homeassistant.components.tractive.* homeassistant.components.tradfri.* homeassistant.components.trafikverket_ferry.* diff --git a/CODEOWNERS b/CODEOWNERS index 93715c3f93b5e7..47fb56ebaf6f50 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1179,6 +1179,8 @@ build.json @home-assistant/supervisor /tests/components/totalconnect/ @austinmroczek /homeassistant/components/tplink/ @rytilahti @thegardenmonkey /tests/components/tplink/ @rytilahti @thegardenmonkey +/homeassistant/components/tplink_omada/ @MarkGodwin +/tests/components/tplink_omada/ @MarkGodwin /homeassistant/components/traccar/ @ludeeus /tests/components/traccar/ @ludeeus /homeassistant/components/trace/ @home-assistant/core diff --git a/homeassistant/components/tplink_omada/__init__.py b/homeassistant/components/tplink_omada/__init__.py new file mode 100644 index 00000000000000..7b171e7be5c5aa --- /dev/null +++ b/homeassistant/components/tplink_omada/__init__.py @@ -0,0 +1,40 @@ +"""The TP-Link Omada integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .config_flow import OmadaHub +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up TP-Link Omada from a config entry.""" + + hass.data.setdefault(DOMAIN, {}) + + try: + hub = OmadaHub(hass, entry.data) + await hub.authenticate() + except Exception as ex: + raise ConfigEntryNotReady( + f"Omada controller could not be reached: {ex}" + ) from ex + + hass.data[DOMAIN][entry.entry_id] = await hub.get_client() + + 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): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/tplink_omada/config_flow.py b/homeassistant/components/tplink_omada/config_flow.py new file mode 100644 index 00000000000000..2e23326cd58fd9 --- /dev/null +++ b/homeassistant/components/tplink_omada/config_flow.py @@ -0,0 +1,118 @@ +"""Config flow for TP-Link Omada integration.""" +from __future__ import annotations + +import logging +from types import MappingProxyType +from typing import Any + +from tplink_omada_client.exceptions import ( + ConnectionFailed, + OmadaClientException, + RequestFailed, + UnsupportedControllerVersion, +) +from tplink_omada_client.omadaclient import OmadaClient +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_VERIFY_SSL, default=True): bool, + vol.Required("site", default="Default"): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class OmadaHub: + """Omada Controller hub.""" + + def __init__(self, hass: HomeAssistant, data: MappingProxyType[str, Any]) -> None: + """Initialize.""" + self.hass = hass + self.host = data[CONF_HOST] + self.site = data["site"] + self.verify_ssl = bool(data[CONF_VERIFY_SSL]) + self.username = data[CONF_USERNAME] + self.password = data[CONF_PASSWORD] + self.client = None + + async def get_client(self) -> OmadaClient: + """Get the client api for the hub.""" + if not self.client: + websession = async_get_clientsession(self.hass, verify_ssl=self.verify_ssl) + self.client = OmadaClient( + self.host, + self.username, + self.password, + websession=websession, + site=self.site, + ) + return self.client + + async def authenticate(self) -> bool: + """Test if we can authenticate with the host.""" + + client = await self.get_client() + await client.login() + + return True + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + + hub = OmadaHub(hass, MappingProxyType(data)) + + await hub.authenticate() + + # Return info that you want to store in the config entry. + return {"title": f"Omada Controller ({data['site']})"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for TP-Link Omada.""" + + VERSION = 1 + + 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 + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except ConnectionFailed: + errors["base"] = "cannot_connect" + except RequestFailed: + errors["base"] = "invalid_auth" + except UnsupportedControllerVersion: + errors["base"] = "unsupported_controller" + except OmadaClientException: + errors["base"] = "unknown" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/tplink_omada/const.py b/homeassistant/components/tplink_omada/const.py new file mode 100644 index 00000000000000..f63d82c6bb4a0a --- /dev/null +++ b/homeassistant/components/tplink_omada/const.py @@ -0,0 +1,3 @@ +"""Constants for the TP-Link Omada integration.""" + +DOMAIN = "tplink_omada" diff --git a/homeassistant/components/tplink_omada/coordinator.py b/homeassistant/components/tplink_omada/coordinator.py new file mode 100644 index 00000000000000..6950e3b6d74a5e --- /dev/null +++ b/homeassistant/components/tplink_omada/coordinator.py @@ -0,0 +1,44 @@ +"""Generic Omada API coordinator.""" +from collections.abc import Awaitable, Callable +from datetime import timedelta +import logging +from typing import Generic, TypeVar + +import async_timeout +from tplink_omada_client.exceptions import OmadaClientException +from tplink_omada_client.omadaclient import OmadaClient + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + +T = TypeVar("T") + + +class OmadaCoordinator(DataUpdateCoordinator[dict[str, T]], Generic[T]): + """Coordinator for synchronizing bulk Omada data.""" + + def __init__( + self, + hass: HomeAssistant, + omada_client: OmadaClient, + update_func: Callable[[OmadaClient], Awaitable[dict[str, T]]], + ) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Omada API Data", + update_interval=timedelta(seconds=300), + ) + self.omada_client = omada_client + self._update_func = update_func + + async def _async_update_data(self) -> dict[str, T]: + """Fetch data from API endpoint.""" + try: + async with async_timeout.timeout(10): + return await self._update_func(self.omada_client) + except OmadaClientException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/tplink_omada/entity.py b/homeassistant/components/tplink_omada/entity.py new file mode 100644 index 00000000000000..3e7f21409bce8c --- /dev/null +++ b/homeassistant/components/tplink_omada/entity.py @@ -0,0 +1,33 @@ +"""Base entity definitions.""" +from tplink_omada_client.devices import OmadaSwitch, OmadaSwitchPortDetails + +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import OmadaCoordinator + + +class OmadaSwitchDeviceEntity( + CoordinatorEntity[OmadaCoordinator[OmadaSwitchPortDetails]] +): + """Common base class for all entities attached to Omada network switches.""" + + def __init__( + self, coordinator: OmadaCoordinator[OmadaSwitchPortDetails], device: OmadaSwitch + ) -> None: + """Initialize the switch.""" + super().__init__(coordinator) + self.device = device + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return DeviceInfo( + connections={(device_registry.CONNECTION_NETWORK_MAC, self.device.mac)}, + identifiers={(DOMAIN, (self.device.mac))}, + manufacturer="TP-Link", + model=self.device.model_display_name, + name=self.device.name, + ) diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json new file mode 100644 index 00000000000000..d0482bfc4a39bf --- /dev/null +++ b/homeassistant/components/tplink_omada/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "tplink_omada", + "name": "TP-Link Omada", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/tplink_omada", + "requirements": ["tplink-omada-client==1.0.0"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@MarkGodwin"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/tplink_omada/strings.json b/homeassistant/components/tplink_omada/strings.json new file mode 100644 index 00000000000000..4e0677727709c2 --- /dev/null +++ b/homeassistant/components/tplink_omada/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "site": "Site", + "username": "[%key:common::config_flow::data::username%]", + "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%]", + "unsupported_controller": "Omada Controller version not supported.", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/tplink_omada/switch.py b/homeassistant/components/tplink_omada/switch.py new file mode 100644 index 00000000000000..2b328ee06bfd2d --- /dev/null +++ b/homeassistant/components/tplink_omada/switch.py @@ -0,0 +1,126 @@ +"""Support for TPLink Omada device toggle options.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from typing import Any + +from tplink_omada_client.definitions import PoEMode +from tplink_omada_client.devices import OmadaSwitch, OmadaSwitchPortDetails +from tplink_omada_client.omadaclient import OmadaClient, SwitchPortOverrides + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import OmadaCoordinator +from .entity import OmadaSwitchDeviceEntity + +POE_SWITCH_ICON = "mdi:ethernet" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches.""" + omada_client: OmadaClient = hass.data[DOMAIN][config_entry.entry_id] + + # Naming fun. Omada switches, as in the network hardware + network_switches = await omada_client.get_switches() + + entities: list = [] + for switch in network_switches: + + def make_update_func( + network_switch: OmadaSwitch, + ) -> Callable[[OmadaClient], Awaitable[dict[str, OmadaSwitchPortDetails]]]: + async def update_func( + client: OmadaClient, + ) -> dict[str, OmadaSwitchPortDetails]: + ports = await client.get_switch_ports(network_switch) + return {p.port_id: p for p in ports} + + return update_func + + coordinator = OmadaCoordinator[OmadaSwitchPortDetails]( + hass, omada_client, make_update_func(switch) + ) + + await coordinator.async_config_entry_first_refresh() + + for port_id in coordinator.data: + entities.append( + OmadaNetworkSwitchPortPoEControl( + coordinator, switch, omada_client, port_id + ) + ) + + async_add_entities(entities) + + +def get_port_base_name(port: OmadaSwitchPortDetails) -> str: + """Get display name for a switch port.""" + + if port.name == f"Port{port.port}": + return f"Port {port.port}" + return f"Port {port.port} ({port.name})" + + +class OmadaNetworkSwitchPortPoEControl(OmadaSwitchDeviceEntity, SwitchEntity): + """Representation of a PoE control toggle on a single network port on a switch.""" + + def __init__( + self, + coordinator: OmadaCoordinator[OmadaSwitchPortDetails], + device: OmadaSwitch, + omada_client: OmadaClient, + port_id: str, + ) -> None: + """Initialize the PoE switch.""" + super().__init__(coordinator, device) + self.port_id = port_id + self.port_details = self.coordinator.data[port_id] + self.omada_client = omada_client + self._attr_unique_id = f"{device.mac}_{port_id}_poe" + + port_name = f"{get_port_base_name(self.port_details)} PoE" + + self.entity_description = SwitchEntityDescription( + f"PoE Enabled Port {self.port_details.port}", + name=port_name, + has_entity_name=True, + entity_category=EntityCategory.CONFIG, + icon=POE_SWITCH_ICON, + ) + self._refresh_state() + + async def _async_turn_on_off_poe(self, enable: bool) -> None: + self.port_details = await self.omada_client.update_switch_port( + self.device, + self.port_details, + overrides=SwitchPortOverrides(enable_poe=enable), + ) + self._refresh_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self._async_turn_on_off_poe(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self._async_turn_on_off_poe(False) + + def _refresh_state(self) -> None: + self._attr_is_on = self.port_details.poe_mode != PoEMode.DISABLED + if self.hass: + self.async_write_ha_state() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.port_details = self.coordinator.data[self.port_id] + self._refresh_state() diff --git a/homeassistant/components/tplink_omada/translations/en.json b/homeassistant/components/tplink_omada/translations/en.json new file mode 100644 index 00000000000000..9447c95b19ebde --- /dev/null +++ b/homeassistant/components/tplink_omada/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "site": "Site", + "verify_ssl": "Verify SSL certificate", + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cdcea6cf9b3383..a3e0e0627889c8 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -420,6 +420,7 @@ "toon", "totalconnect", "tplink", + "tplink_omada", "traccar", "tractive", "tradfri", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index aa6221a114003e..45e039963fde40 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5479,6 +5479,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "tplink_omada": { + "name": "TP-Link Omada", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "traccar": { "name": "Traccar", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index e30a78dab8c0e8..267f3b874bef7e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2434,6 +2434,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.tplink_omada.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.tractive.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 20ea4c7862efb9..241c6ffa2e213d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2450,6 +2450,9 @@ total_connect_client==2022.10 # homeassistant.components.tplink_lte tp-connected==0.0.4 +# homeassistant.components.tplink_omada +tplink-omada-client==1.0.0 + # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f365317a7adfc1..cc9a646a819696 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1687,6 +1687,9 @@ toonapi==0.2.1 # homeassistant.components.totalconnect total_connect_client==2022.10 +# homeassistant.components.tplink_omada +tplink-omada-client==1.0.0 + # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/tplink_omada/__init__.py b/tests/components/tplink_omada/__init__.py new file mode 100644 index 00000000000000..10b2c72c35b03b --- /dev/null +++ b/tests/components/tplink_omada/__init__.py @@ -0,0 +1 @@ +"""Tests for the TP-Link Omada integration.""" diff --git a/tests/components/tplink_omada/test_config_flow.py b/tests/components/tplink_omada/test_config_flow.py new file mode 100644 index 00000000000000..348ddd70780bbd --- /dev/null +++ b/tests/components/tplink_omada/test_config_flow.py @@ -0,0 +1,98 @@ +"""Test the TP-Link Omada config flow.""" +from unittest.mock import patch + +from tplink_omada_client.exceptions import ConnectionFailed, RequestFailed + +from homeassistant import config_entries +from homeassistant.components.tplink_omada.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +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.tplink_omada.config_flow.OmadaHub.authenticate", + return_value=True, + ), patch( + "homeassistant.components.tplink_omada.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "verify_ssl": True, + "site": "Test", + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Omada Controller (Test)" + assert result2["data"] == { + "host": "1.1.1.1", + "verify_ssl": True, + "site": "Test", + "username": "test-username", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +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.tplink_omada.config_flow.PlaceholderHub.authenticate", + side_effect=RequestFailed, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "site": "Test", + "verify_ssl": False, + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> 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.tplink_omada.config_flow.PlaceholderHub.authenticate", + side_effect=ConnectionFailed, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "site": "Test", + "verify_ssl": True, + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} From cb0a5b624d46639134a2f411eab4818456925d66 Mon Sep 17 00:00:00 2001 From: "mark.r.godwin@gmail.com" Date: Sat, 29 Oct 2022 20:28:35 +0000 Subject: [PATCH 393/394] Bump omada client version --- .../components/tplink_omada/__init__.py | 2 +- .../components/tplink_omada/config_flow.py | 20 +++++++---- .../components/tplink_omada/manifest.json | 2 +- .../components/tplink_omada/switch.py | 15 +++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../tplink_omada/test_config_flow.py | 33 +++++++++++++++++-- 7 files changed, 58 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/tplink_omada/__init__.py b/homeassistant/components/tplink_omada/__init__.py index 7b171e7be5c5aa..b688cf3ae0a0e3 100644 --- a/homeassistant/components/tplink_omada/__init__.py +++ b/homeassistant/components/tplink_omada/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Omada controller could not be reached: {ex}" ) from ex - hass.data[DOMAIN][entry.entry_id] = await hub.get_client() + hass.data[DOMAIN][entry.entry_id] = hub.get_client() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tplink_omada/config_flow.py b/homeassistant/components/tplink_omada/config_flow.py index 2e23326cd58fd9..9086a93b404423 100644 --- a/homeassistant/components/tplink_omada/config_flow.py +++ b/homeassistant/components/tplink_omada/config_flow.py @@ -7,8 +7,8 @@ from tplink_omada_client.exceptions import ( ConnectionFailed, + LoginFailed, OmadaClientException, - RequestFailed, UnsupportedControllerVersion, ) from tplink_omada_client.omadaclient import OmadaClient @@ -48,7 +48,7 @@ def __init__(self, hass: HomeAssistant, data: MappingProxyType[str, Any]) -> Non self.password = data[CONF_PASSWORD] self.client = None - async def get_client(self) -> OmadaClient: + def get_client(self) -> OmadaClient: """Get the client api for the hub.""" if not self.client: websession = async_get_clientsession(self.hass, verify_ssl=self.verify_ssl) @@ -64,11 +64,17 @@ async def get_client(self) -> OmadaClient: async def authenticate(self) -> bool: """Test if we can authenticate with the host.""" - client = await self.get_client() + client = self.get_client() await client.login() return True + async def get_controller_name(self) -> str: + """Get the display name of the controller.""" + client = self.get_client() + name = await client.get_controller_name() + return str(name) + async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" @@ -76,9 +82,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, hub = OmadaHub(hass, MappingProxyType(data)) await hub.authenticate() + name = await hub.get_controller_name() # Return info that you want to store in the config entry. - return {"title": f"Omada Controller ({data['site']})"} + return {"title": f"TP-Link Omada {name} ({data['site']})"} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -101,11 +108,12 @@ async def async_step_user( info = await validate_input(self.hass, user_input) except ConnectionFailed: errors["base"] = "cannot_connect" - except RequestFailed: + except LoginFailed: errors["base"] = "invalid_auth" except UnsupportedControllerVersion: errors["base"] = "unsupported_controller" - except OmadaClientException: + except OmadaClientException as ex: + _LOGGER.exception("Unexpected API error: %s", ex) errors["base"] = "unknown" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json index d0482bfc4a39bf..bda2466ad54c93 100644 --- a/homeassistant/components/tplink_omada/manifest.json +++ b/homeassistant/components/tplink_omada/manifest.json @@ -3,7 +3,7 @@ "name": "TP-Link Omada", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink_omada", - "requirements": ["tplink-omada-client==1.0.0"], + "requirements": ["tplink-omada-client==1.0.1"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/homeassistant/components/tplink_omada/switch.py b/homeassistant/components/tplink_omada/switch.py index 2b328ee06bfd2d..e1f3c34860241b 100644 --- a/homeassistant/components/tplink_omada/switch.py +++ b/homeassistant/components/tplink_omada/switch.py @@ -33,7 +33,9 @@ async def async_setup_entry( network_switches = await omada_client.get_switches() entities: list = [] - for switch in network_switches: + for switch in [ + ns for ns in network_switches if ns.device_capabilities.supports_poe + ]: def make_update_func( network_switch: OmadaSwitch, @@ -52,12 +54,13 @@ async def update_func( await coordinator.async_config_entry_first_refresh() - for port_id in coordinator.data: - entities.append( - OmadaNetworkSwitchPortPoEControl( - coordinator, switch, omada_client, port_id + for idx, port_id in enumerate(coordinator.data): + if idx < switch.device_capabilities.poe_ports: + entities.append( + OmadaNetworkSwitchPortPoEControl( + coordinator, switch, omada_client, port_id + ) ) - ) async_add_entities(entities) diff --git a/requirements_all.txt b/requirements_all.txt index 241c6ffa2e213d..01c5af78dd3301 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2451,7 +2451,7 @@ total_connect_client==2022.10 tp-connected==0.0.4 # homeassistant.components.tplink_omada -tplink-omada-client==1.0.0 +tplink-omada-client==1.0.1 # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc9a646a819696..cd630c6606f490 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1688,7 +1688,7 @@ toonapi==0.2.1 total_connect_client==2022.10 # homeassistant.components.tplink_omada -tplink-omada-client==1.0.0 +tplink-omada-client==1.0.1 # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/tplink_omada/test_config_flow.py b/tests/components/tplink_omada/test_config_flow.py index 348ddd70780bbd..a198f3165bea47 100644 --- a/tests/components/tplink_omada/test_config_flow.py +++ b/tests/components/tplink_omada/test_config_flow.py @@ -1,7 +1,11 @@ """Test the TP-Link Omada config flow.""" from unittest.mock import patch -from tplink_omada_client.exceptions import ConnectionFailed, RequestFailed +from tplink_omada_client.exceptions import ( + ConnectionFailed, + LoginFailed, + OmadaClientException, +) from homeassistant import config_entries from homeassistant.components.tplink_omada.const import DOMAIN @@ -56,7 +60,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: with patch( "homeassistant.components.tplink_omada.config_flow.PlaceholderHub.authenticate", - side_effect=RequestFailed, + side_effect=LoginFailed, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -73,6 +77,31 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_auth"} +async def test_form_api_error(hass: HomeAssistant) -> None: + """Test we handle unknown API error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tplink_omada.config_flow.PlaceholderHub.authenticate", + side_effect=OmadaClientException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "site": "Test", + "verify_ssl": False, + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unknown"} + + async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( From 6edb2e527661b388bf8994e54de5c986996ab686 Mon Sep 17 00:00:00 2001 From: "mark.r.godwin@gmail.com" Date: Sun, 30 Oct 2022 16:21:34 +0000 Subject: [PATCH 394/394] Fixing tests --- .../components/tplink_omada/manifest.json | 1 + .../tplink_omada/test_config_flow.py | 39 ++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tplink_omada/manifest.json b/homeassistant/components/tplink_omada/manifest.json index bda2466ad54c93..46c81c6fe1cdaf 100644 --- a/homeassistant/components/tplink_omada/manifest.json +++ b/homeassistant/components/tplink_omada/manifest.json @@ -3,6 +3,7 @@ "name": "TP-Link Omada", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink_omada", + "integration_type": "hub", "requirements": ["tplink-omada-client==1.0.1"], "ssdp": [], "zeroconf": [], diff --git a/tests/components/tplink_omada/test_config_flow.py b/tests/components/tplink_omada/test_config_flow.py index a198f3165bea47..1683fb17cc878e 100644 --- a/tests/components/tplink_omada/test_config_flow.py +++ b/tests/components/tplink_omada/test_config_flow.py @@ -5,6 +5,7 @@ ConnectionFailed, LoginFailed, OmadaClientException, + UnsupportedControllerVersion, ) from homeassistant import config_entries @@ -24,6 +25,9 @@ async def test_form(hass: HomeAssistant) -> None: with patch( "homeassistant.components.tplink_omada.config_flow.OmadaHub.authenticate", return_value=True, + ), patch( + "homeassistant.components.tplink_omada.config_flow.OmadaHub.get_controller_name", + return_value="OC200", ), patch( "homeassistant.components.tplink_omada.async_setup_entry", return_value=True, @@ -41,7 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Omada Controller (Test)" + assert result2["title"] == "TP-Link Omada OC200 (Test)" assert result2["data"] == { "host": "1.1.1.1", "verify_ssl": True, @@ -59,8 +63,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.tplink_omada.config_flow.PlaceholderHub.authenticate", - side_effect=LoginFailed, + "homeassistant.components.tplink_omada.config_flow.OmadaHub.authenticate", + side_effect=LoginFailed(-1000, "Invalid username/password"), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -84,7 +88,7 @@ async def test_form_api_error(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.tplink_omada.config_flow.PlaceholderHub.authenticate", + "homeassistant.components.tplink_omada.config_flow.OmadaHub.authenticate", side_effect=OmadaClientException, ): result2 = await hass.config_entries.flow.async_configure( @@ -102,6 +106,31 @@ async def test_form_api_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "unknown"} +async def test_form_unsupported_controller(hass: HomeAssistant) -> None: + """Test we handle unknown API error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tplink_omada.config_flow.OmadaHub.authenticate", + side_effect=UnsupportedControllerVersion("4.0.0"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "site": "Test", + "verify_ssl": False, + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "unsupported_controller"} + + async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -109,7 +138,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.tplink_omada.config_flow.PlaceholderHub.authenticate", + "homeassistant.components.tplink_omada.config_flow.OmadaHub.authenticate", side_effect=ConnectionFailed, ): result2 = await hass.config_entries.flow.async_configure(